def usedTilesets(self): tilesets = QSet() for object in self.mObjects: tile = object.cell().tile if tile: tilesets.insert(tile.sharedTileset()) return tilesets
def usedTilesets(self): tilesets = QSet() i = 0 while(i<self.mGrid.size()): tile = self.mGrid.at(i).tile if tile: tilesets.insert(tile.tileset()) i += 1 return tilesets
def usedTilesets(self): tilesets = QSet() i = 0 while (i < self.mGrid.size()): tile = self.mGrid.at(i).tile if tile: tilesets.insert(tile.tileset()) i += 1 return tilesets
def updateSelectedObjectItems(self): objects = self.mMapDocument.selectedObjects() items = QSet() for object in objects: item = self.itemForObject(object) if item: items.insert(item) self.mSelectedObjectItems = items self.selectedObjectItemsChanged.emit()
def updateSelection(self, pos, modifiers): rect = QRectF(self.mStart, pos).normalized() # Make sure the rect has some contents, otherwise intersects returns False rect.setWidth(max(1.0, rect.width())) rect.setHeight(max(1.0, rect.height())) selectedItems = QSet() for item in self.mapScene().items(rect): if type(item) == MapObjectItem: selectedItems.insert(item) if (modifiers & (Qt.ControlModifier | Qt.ShiftModifier)): selectedItems |= self.mapScene().selectedObjectItems() else: self.setMode(Mode.Resize) self.mapScene().setSelectedObjectItems(selectedItems)
def __init__(self): super().__init__() ## # Stores the tilesets and maps them to the number of references. ## self.mTilesets = QMap() self.mChangedFiles = QSet() self.mWatcher = FileSystemWatcher(self) self.mAnimationDriver = TileAnimationDriver(self) self.mReloadTilesetsOnChange = False self.mChangedFilesTimer = QTimer() self.mWatcher.fileChanged.connect(self.fileChanged) self.mChangedFilesTimer.setInterval(500) self.mChangedFilesTimer.setSingleShot(True) self.mChangedFilesTimer.timeout.connect(self.fileChangedTimeout) self.mAnimationDriver.update.connect(self.advanceTileAnimations)
def __init__(self, parent): super().__init__(parent) self.mMapDocument = None self.mSelectedTool = None self.mActiveTool = None self.mObjectSelectionItem = None self.mUnderMouse = False self.mCurrentModifiers = Qt.NoModifier, self.mDarkRectangle = QGraphicsRectItem() self.mDefaultBackgroundColor = Qt.darkGray self.mLayerItems = QVector() self.mObjectItems = QMap() self.mObjectLineWidth = 0.0 self.mSelectedObjectItems = QSet() self.mLastMousePos = QPointF() self.mShowTileObjectOutlines = False self.mHighlightCurrentLayer = False self.mGridVisible = False self.setBackgroundBrush(self.mDefaultBackgroundColor) tilesetManager = TilesetManager.instance() tilesetManager.tilesetChanged.connect(self.tilesetChanged) tilesetManager.repaintTileset.connect(self.tilesetChanged) prefs = preferences.Preferences.instance() prefs.showGridChanged.connect(self.setGridVisible) prefs.showTileObjectOutlinesChanged.connect( self.setShowTileObjectOutlines) prefs.objectTypesChanged.connect(self.syncAllObjectItems) prefs.highlightCurrentLayerChanged.connect( self.setHighlightCurrentLayer) prefs.gridColorChanged.connect(self.update) prefs.objectLineWidthChanged.connect(self.setObjectLineWidth) self.mDarkRectangle.setPen(QPen(Qt.NoPen)) self.mDarkRectangle.setBrush(Qt.black) self.mDarkRectangle.setOpacity(darkeningFactor) self.addItem(self.mDarkRectangle) self.mGridVisible = prefs.showGrid() self.mObjectLineWidth = prefs.objectLineWidth() self.mShowTileObjectOutlines = prefs.showTileObjectOutlines() self.mHighlightCurrentLayer = prefs.highlightCurrentLayer() # Install an event filter so that we can get key events on behalf of the # active tool without having to have the current focus. QCoreApplication.instance().installEventFilter(self)
def updateSelection(self, event): rect = QRectF(self.mStart, event.scenePos()).normalized() # Make sure the rect has some contents, otherwise intersects returns False rect.setWidth(max(1.0, rect.width())) rect.setHeight(max(1.0, rect.height())) oldSelection = self.mapScene().selectedObjectItems() if (oldSelection.isEmpty()): # Allow selecting some map objects only when there aren't any selected selectedItems = QSet() for item in self.mapScene().items(rect, Qt.IntersectsItemShape, Qt.DescendingOrder, viewTransform(event)): if type(item) == MapObjectItem: selectedItems.insert(item) newSelection = QSet() if (event.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier)): newSelection = oldSelection | selectedItems else: newSelection = selectedItems self.mapScene().setSelectedObjectItems(newSelection) self.updateHandles() else: # Update the selected handles selectedHandles = QSet() for item in self.mapScene().items(rect, Qt.IntersectsItemShape, Qt.DescendingOrder, viewTransform(event)): if type(item) == PointHandle: selectedHandles.insert(item) if (event.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier)): self.setSelectedHandles(self.mSelectedHandles | selectedHandles) else: self.setSelectedHandles(selectedHandles)
def __init__(self, mapDocument, autoMapper, where): super().__init__() self.mLayersAfter = QVector() self.mLayersBefore = QVector() self.mMapDocument = mapDocument map = self.mMapDocument.Map() touchedLayers = QSet() index = 0 while (index < autoMapper.size()): a = autoMapper.at(index) if (a.prepareAutoMap()): touchedLayers |= a.getTouchedTileLayers() index += 1 else: autoMapper.remove(index) for layerName in touchedLayers: layerindex = map.indexOfLayer(layerName) self.mLayersBefore(map.layerAt(layerindex).clone()) for a in autoMapper: a.autoMap(where) for layerName in touchedLayers: layerindex = map.indexOfLayer(layerName) # layerindex exists, because AutoMapper is still alive, dont check self.mLayersAfter(map.layerAt(layerindex).clone()) # reduce memory usage by saving only diffs for i in range(self.mLayersAfter.size()): before = self.mLayersBefore.at(i) after = self.mLayersAfter.at(i) diffRegion = before.computeDiffRegion(after).boundingRect() before1 = before.copy(diffRegion) after1 = after.copy(diffRegion) before1.setPosition(diffRegion.topLeft()) after1.setPosition(diffRegion.topLeft()) before1.setName(before.name()) after1.setName(after.name()) self.mLayersBefore.replace(i, before1) self.mLayersAfter.replace(i, after1) del before del after for a in autoMapper: a.cleanAll()
def mouseReleased(self, event): if (event.button() != Qt.LeftButton): return x = self.mAction if x == Action.NoAction: if (not self.mClickedRotateHandle and not self.mClickedResizeHandle): # Don't change selection as a result of clicking on a handle modifiers = event.modifiers() if (self.mClickedObjectItem): selection = self.mapScene().selectedObjectItems() if (modifiers & (Qt.ShiftModifier | Qt.ControlModifier)): if (selection.contains(self.mClickedObjectItem)): selection.remove(self.mClickedObjectItem) else: selection.insert(self.mClickedObjectItem) elif (selection.contains(self.mClickedObjectItem)): # Clicking one of the selected items changes the edit mode if self.mMode == Mode.Resize: _x = Mode.Rotate else: _x = Mode.Resize self.setMode(_x) else: selection.clear() selection.insert(self.mClickedObjectItem) self.setMode(Mode.Resize) self.mapScene().setSelectedObjectItems(selection) elif (not (modifiers & Qt.ShiftModifier)): self.mapScene().setSelectedObjectItems(QSet()) elif x == Action.Selecting: self.updateSelection(event.scenePos(), event.modifiers()) self.mapScene().removeItem(self.mSelectionRectangle) self.mAction = Action.NoAction elif x == Action.Moving: self.finishMoving(event.scenePos()) elif x == Action.Rotating: self.finishRotating(event.scenePos()) elif x == Action.Resizing: self.finishResizing(event.scenePos()) self.mMousePressed = False self.mClickedObjectItem = None self.mClickedRotateHandle = None self.mClickedResizeHandle = None self.refreshCursor()
def startMoving(self, modifiers): # Move only the clicked item, if it was not part of the selection if (self.mClickedObjectItem and not (modifiers & Qt.AltModifier)): if (not self.mapScene().selectedObjectItems().contains( self.mClickedObjectItem)): self.mapScene().setSelectedObjectItems( QSet([self.mClickedObjectItem])) self.saveSelectionState() self.mAction = Action.Moving self.mAlignPosition = self.mMovingObjects[0].oldPosition for object in self.mMovingObjects: pos = object.oldPosition if (pos.x() < self.mAlignPosition.x()): self.mAlignPosition.setX(pos.x()) if (pos.y() < self.mAlignPosition.y()): self.mAlignPosition.setY(pos.y()) self.updateHandleVisibility()
def selectTilesInStamp(self, stamp): if self.mEmittingStampCaptured: return processed = QSet() selections = QMap() for variation in stamp.variations(): tileLayer = variation.tileLayer() for cell in tileLayer: tile = cell.tile if tile: if (processed.contains(tile)): continue processed.insert(tile) # avoid spending time on duplicates tileset = tile.tileset() tilesetIndex = self.mTilesets.indexOf( tileset.sharedPointer()) if (tilesetIndex != -1): view = self.tilesetViewAt(tilesetIndex) if (not view.model()): # Lazily set up the model self.setupTilesetModel(view, tileset) model = view.tilesetModel() modelIndex = model.tileIndex(tile) selectionModel = view.selectionModel() _x = QItemSelection() _x.select(modelIndex, modelIndex) selections[selectionModel] = _x if (not selections.isEmpty()): self.mSynchronizingSelection = True # Mark captured tiles as selected for i in selections: selectionModel = i[0] selection = i[1] selectionModel.select(selection, QItemSelectionModel.SelectCurrent) # Show/edit properties of all captured tiles self.mMapDocument.setSelectedTiles(processed.toList()) # Update the current tile (useful for animation and collision editors) first = selections.first() selectionModel = first[0] selection = first[1] currentIndex = QModelIndex(selection.first().topLeft()) if (selectionModel.currentIndex() != currentIndex): selectionModel.setCurrentIndex(currentIndex, QItemSelectionModel.NoUpdate) else: self.currentChanged(currentIndex) self.mSynchronizingSelection = False
def __init__(self, parent): super().__init__(parent) self.mMapDocument = None self.mSelectedTool = None self.mActiveTool = None self.mObjectSelectionItem = None self.mUnderMouse = False self.mCurrentModifiers = Qt.NoModifier, self.mDarkRectangle = QGraphicsRectItem() self.mDefaultBackgroundColor = Qt.darkGray self.mLayerItems = QVector() self.mObjectItems = QMap() self.mObjectLineWidth = 0.0 self.mSelectedObjectItems = QSet() self.mLastMousePos = QPointF() self.mShowTileObjectOutlines = False self.mHighlightCurrentLayer = False self.mGridVisible = False self.setBackgroundBrush(self.mDefaultBackgroundColor) tilesetManager = TilesetManager.instance() tilesetManager.tilesetChanged.connect(self.tilesetChanged) tilesetManager.repaintTileset.connect(self.tilesetChanged) prefs = preferences.Preferences.instance() prefs.showGridChanged.connect(self.setGridVisible) prefs.showTileObjectOutlinesChanged.connect(self.setShowTileObjectOutlines) prefs.objectTypesChanged.connect(self.syncAllObjectItems) prefs.highlightCurrentLayerChanged.connect(self.setHighlightCurrentLayer) prefs.gridColorChanged.connect(self.update) prefs.objectLineWidthChanged.connect(self.setObjectLineWidth) self.mDarkRectangle.setPen(QPen(Qt.NoPen)) self.mDarkRectangle.setBrush(Qt.black) self.mDarkRectangle.setOpacity(darkeningFactor) self.addItem(self.mDarkRectangle) self.mGridVisible = prefs.showGrid() self.mObjectLineWidth = prefs.objectLineWidth() self.mShowTileObjectOutlines = prefs.showTileObjectOutlines() self.mHighlightCurrentLayer = prefs.highlightCurrentLayer() # Install an event filter so that we can get key events on behalf of the # active tool without having to have the current focus. QCoreApplication.instance().installEventFilter(self)
def __init__(self, parent = None): super().__init__(self.tr("Edit Polygons"), QIcon(":images/24x24/tool-edit-polygons.png"), QKeySequence(self.tr("E")), parent) self.mSelectedHandles = QSet() self.mModifiers = Qt.KeyboardModifiers() self.mScreenStart = QPoint() self.mOldHandlePositions = QVector() self.mAlignPosition = QPointF() ## The list of handles associated with each selected map object self.mHandles = QMapList() self.mOldPolygons = QMap() self.mStart = QPointF() self.mSelectionRectangle = SelectionRectangle() self.mMousePressed = False self.mClickedHandle = None self.mClickedObjectItem = None self.mMode = EditPolygonTool.NoMode
def __init__(self): self.names = QSet()
def __init__(self): self.indexes = QSet() self.names = QSet() # all names
class EditPolygonTool(AbstractObjectTool): NoMode, Selecting, Moving = range(3) def __init__(self, parent = None): super().__init__(self.tr("Edit Polygons"), QIcon(":images/24x24/tool-edit-polygons.png"), QKeySequence(self.tr("E")), parent) self.mSelectedHandles = QSet() self.mModifiers = Qt.KeyboardModifiers() self.mScreenStart = QPoint() self.mOldHandlePositions = QVector() self.mAlignPosition = QPointF() ## The list of handles associated with each selected map object self.mHandles = QMapList() self.mOldPolygons = QMap() self.mStart = QPointF() self.mSelectionRectangle = SelectionRectangle() self.mMousePressed = False self.mClickedHandle = None self.mClickedObjectItem = None self.mMode = EditPolygonTool.NoMode def __del__(self): del self.mSelectionRectangle def tr(self, sourceText, disambiguation = '', n = -1): return QCoreApplication.translate('EditPolygonTool', sourceText, disambiguation, n) def activate(self, scene): super().activate(scene) self.updateHandles() # TODO: Could be more optimal by separating the updating of handles from # the creation and removal of handles depending on changes in the # selection, and by only updating the handles of the objects that changed. self.mapDocument().objectsChanged.connect(self.updateHandles) scene.selectedObjectItemsChanged.connect(self.updateHandles) self.mapDocument().objectsRemoved.connect(self.objectsRemoved) def deactivate(self, scene): try: self.mapDocument().objectsChanged.disconnect(self.updateHandles) scene.selectedObjectItemsChanged.disconnect(self.updateHandles) except: pass # Delete all handles self.mHandles.clear() self.mSelectedHandles.clear() self.mClickedHandle = None super().deactivate(scene) def mouseEntered(self): pass def mouseMoved(self, pos, modifiers): super().mouseMoved(pos, modifiers) if (self.mMode == EditPolygonTool.NoMode and self.mMousePressed): screenPos = QCursor.pos() dragDistance = (self.mScreenStart - screenPos).manhattanLength() if (dragDistance >= QApplication.startDragDistance()): if (self.mClickedHandle): self.startMoving() else: self.startSelecting() x = self.mMode if x==EditPolygonTool.Selecting: self.mSelectionRectangle.setRectangle(QRectF(self.mStart, pos).normalized()) elif x==EditPolygonTool.Moving: self.updateMovingItems(pos, modifiers) elif x==EditPolygonTool.NoMode: pass def mousePressed(self, event): if (self.mMode != EditPolygonTool.NoMode): # Ignore additional presses during select/move return x = event.button() if x==Qt.LeftButton: self.mMousePressed = True self.mStart = event.scenePos() self.mScreenStart = event.screenPos() items = self.mapScene().items(self.mStart, Qt.IntersectsItemShape, Qt.DescendingOrder, viewTransform(event)) self.mClickedObjectItem = first(items, MapObjectItem) self.mClickedHandle = first(items, PointHandle) elif x==Qt.RightButton: items = self.mapScene().items(event.scenePos(), Qt.IntersectsItemShape, Qt.DescendingOrder, viewTransform(event)) clickedHandle = first(items) if (clickedHandle or not self.mSelectedHandles.isEmpty()): self.showHandleContextMenu(clickedHandle, event.screenPos()) else: super().mousePressed(event) else: super().mousePressed(event) def mouseReleased(self, event): if (event.button() != Qt.LeftButton): return x = self.mMode if x==EditPolygonTool.NoMode: if (self.mClickedHandle): selection = self.mSelectedHandles modifiers = event.modifiers() if (modifiers & (Qt.ShiftModifier | Qt.ControlModifier)): if (selection.contains(self.mClickedHandle)): selection.remove(self.mClickedHandle) else: selection.insert(self.mClickedHandle) else: selection.clear() selection.insert(self.mClickedHandle) self.setSelectedHandles(selection) elif (self.mClickedObjectItem): selection = self.mapScene().selectedObjectItems() modifiers = event.modifiers() if (modifiers & (Qt.ShiftModifier | Qt.ControlModifier)): if (selection.contains(self.mClickedObjectItem)): selection.remove(self.mClickedObjectItem) else: selection.insert(self.mClickedObjectItem) else: selection.clear() selection.insert(self.mClickedObjectItem) self.mapScene().setSelectedObjectItems(selection) self.updateHandles() elif (not self.mSelectedHandles.isEmpty()): # First clear the handle selection self.setSelectedHandles(QSet()) else: # If there is no handle selection, clear the object selection self.mapScene().setSelectedObjectItems(QSet()) self.updateHandles() elif x==EditPolygonTool.Selecting: self.updateSelection(event) self.mapScene().removeItem(self.mSelectionRectangle) self.mMode = EditPolygonTool.NoMode elif x==EditPolygonTool.Moving: self.finishMoving(event.scenePos()) self.mMousePressed = False self.mClickedHandle = None def modifiersChanged(self, modifiers): self.mModifiers = modifiers def languageChanged(self): self.setName(self.tr("Edit Polygons")) self.setShortcut(QKeySequence(self.tr("E"))) def updateHandles(self): selection = self.mapScene().selectedObjectItems() # First destroy the handles for objects that are no longer selected for l in range(len(self.mHandles)): i = self.mHandles.itemByIndex(l) if (not selection.contains(i[0])): for handle in i[1]: if (handle.isSelected()): self.mSelectedHandles.remove(handle) del handle del self.mHandles[l] renderer = self.mapDocument().renderer() for item in selection: object = item.mapObject() if (not object.cell().isEmpty()): continue polygon = object.polygon() polygon.translate(object.position()) pointHandles = self.mHandles.get(item) # Create missing handles while (pointHandles.size() < polygon.size()): handle = PointHandle(item, pointHandles.size()) pointHandles.append(handle) self.mapScene().addItem(handle) # Remove superfluous handles while (pointHandles.size() > polygon.size()): handle = pointHandles.takeLast() if (handle.isSelected()): self.mSelectedHandles.remove(handle) del handle # Update the position of all handles for i in range(pointHandles.size()): point = polygon.at(i) handlePos = renderer.pixelToScreenCoords_(point) internalHandlePos = handlePos - item.pos() pointHandles.at(i).setPos(item.mapToScene(internalHandlePos)) self.mHandles.insert(item, pointHandles) def objectsRemoved(self, objects): if (self.mMode == EditPolygonTool.Moving): # Make sure we're not going to try to still change these objects when # finishing the move operation. # TODO: In addition to avoiding crashes, it would also be good to # disallow other actions while moving. for object in objects: self.mOldPolygons.remove(object) def deleteNodes(self): if (self.mSelectedHandles.isEmpty()): return p = groupIndexesByObject(self.mSelectedHandles) undoStack = self.mapDocument().undoStack() delText = self.tr("Delete %n Node(s)", "", self.mSelectedHandles.size()) undoStack.beginMacro(delText) for i in p: object = i[0] indexRanges = i[1] oldPolygon = object.polygon() newPolygon = oldPolygon # Remove points, back to front to keep the indexes valid it = indexRanges.end() begin = indexRanges.begin() # assert: end != begin, since there is at least one entry while(it != begin): it -= 1 newPolygon.remove(it.first(), it.length()) if (newPolygon.size() < 2): # We've removed the entire object undoStack.push(RemoveMapObject(self.mapDocument(), object)) else: undoStack.push(ChangePolygon(self.mapDocument(), object, newPolygon, oldPolygon)) undoStack.endMacro() def joinNodes(self): if (self.mSelectedHandles.size() < 2): return p = groupIndexesByObject(self.mSelectedHandles) undoStack = self.mapDocument().undoStack() macroStarted = False for i in p: object = i[0] indexRanges = i[1] closed = object.shape() == MapObject.Polygon oldPolygon = object.polygon() newPolygon = joinPolygonNodes(oldPolygon, indexRanges, closed) if (newPolygon.size() < oldPolygon.size()): if (not macroStarted): undoStack.beginMacro(self.tr("Join Nodes")) macroStarted = True undoStack.push(ChangePolygon(self.mapDocument(), object, newPolygon, oldPolygon)) if (macroStarted): undoStack.endMacro() def splitSegments(self): if (self.mSelectedHandles.size() < 2): return p = groupIndexesByObject(self.mSelectedHandles) undoStack = self.mapDocument().undoStack() macroStarted = False for i in p: object = i[0] indexRanges = i[1] closed = object.shape() == MapObject.Polygon oldPolygon = object.polygon() newPolygon = splitPolygonSegments(oldPolygon, indexRanges, closed) if (newPolygon.size() > oldPolygon.size()): if (not macroStarted): undoStack.beginMacro(self.tr("Split Segments")) macroStarted = True undoStack.push(ChangePolygon(self.mapDocument(), object, newPolygon, oldPolygon)) if (macroStarted): undoStack.endMacro() def setSelectedHandles(self, handles): for handle in self.mSelectedHandles: if (not handles.contains(handle)): handle.setSelected(False) for handle in handles: if (not self.mSelectedHandles.contains(handle)): handle.setSelected(True) self.mSelectedHandles = handles def setSelectedHandle(self, handle): self.setSelectedHandles(QSet([handle])) def updateSelection(self, event): rect = QRectF(self.mStart, event.scenePos()).normalized() # Make sure the rect has some contents, otherwise intersects returns False rect.setWidth(max(1.0, rect.width())) rect.setHeight(max(1.0, rect.height())) oldSelection = self.mapScene().selectedObjectItems() if (oldSelection.isEmpty()): # Allow selecting some map objects only when there aren't any selected selectedItems = QSet() for item in self.mapScene().items(rect, Qt.IntersectsItemShape, Qt.DescendingOrder, viewTransform(event)): if type(item) == MapObjectItem: selectedItems.insert(item) newSelection = QSet() if (event.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier)): newSelection = oldSelection | selectedItems else: newSelection = selectedItems self.mapScene().setSelectedObjectItems(newSelection) self.updateHandles() else: # Update the selected handles selectedHandles = QSet() for item in self.mapScene().items(rect, Qt.IntersectsItemShape, Qt.DescendingOrder, viewTransform(event)): if type(item) == PointHandle: selectedHandles.insert(item) if (event.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier)): self.setSelectedHandles(self.mSelectedHandles | selectedHandles) else: self.setSelectedHandles(selectedHandles) def startSelecting(self): self.mMode = EditPolygonTool.Selecting self.mapScene().addItem(self.mSelectionRectangle) def startMoving(self): # Move only the clicked handle, if it was not part of the selection if (not self.mSelectedHandles.contains(self.mClickedHandle)): self.setSelectedHandle(self.mClickedHandle) self.mMode = EditPolygonTool.Moving renderer = self.mapDocument().renderer() # Remember the current object positions self.mOldHandlePositions.clear() self.mOldPolygons.clear() self.mAlignPosition = renderer.screenToPixelCoords_((self.mSelectedHandles.begin()).pos()) for handle in self.mSelectedHandles: pos = renderer.screenToPixelCoords_(handle.pos()) self.mOldHandlePositions.append(handle.pos()) if (pos.x() < self.mAlignPosition.x()): self.mAlignPosition.setX(pos.x()) if (pos.y() < self.mAlignPosition.y()): self.mAlignPosition.setY(pos.y()) mapObject = handle.mapObject() if (not self.mOldPolygons.contains(mapObject)): self.mOldPolygons.insert(mapObject, mapObject.polygon()) def updateMovingItems(self, pos, modifiers): renderer = self.mapDocument().renderer() diff = pos - self.mStart snapHelper = SnapHelper(renderer, modifiers) if (snapHelper.snaps()): alignScreenPos = renderer.pixelToScreenCoords_(self.mAlignPosition) newAlignScreenPos = alignScreenPos + diff newAlignPixelPos = renderer.screenToPixelCoords_(newAlignScreenPos) snapHelper.snap(newAlignPixelPos) diff = renderer.pixelToScreenCoords_(newAlignPixelPos) - alignScreenPos i = 0 for handle in self.mSelectedHandles: # update handle position newScreenPos = self.mOldHandlePositions.at(i) + diff handle.setPos(newScreenPos) # calculate new pixel position of polygon node item = handle.mapObjectItem() newInternalPos = item.mapFromScene(newScreenPos) newScenePos = item.pos() + newInternalPos newPixelPos = renderer.screenToPixelCoords_(newScenePos) # update the polygon mapObject = item.mapObject() polygon = mapObject.polygon() polygon[handle.pointIndex()] = newPixelPos - mapObject.position() self.mapDocument().mapObjectModel().setObjectPolygon(mapObject, polygon) i += 1 def finishMoving(self, pos): self.mMode = EditPolygonTool.NoMode if (self.mStart == pos or self.mOldPolygons.isEmpty()): # Move is a no-op return undoStack = self.mapDocument().undoStack() undoStack.beginMacro(self.tr("Move %n Point(s)", "", self.mSelectedHandles.size())) # TODO: This isn't really optimal. Would be better to have a single undo # command that supports changing multiple map objects. for i in self.mOldPolygons: undoStack.push(ChangePolygon(self.mapDocument(), i[0], i[1])) undoStack.endMacro() self.mOldHandlePositions.clear() self.mOldPolygons.clear() def showHandleContextMenu(self, clickedHandle, screenPos): if (clickedHandle and not self.mSelectedHandles.contains(clickedHandle)): self.setSelectedHandle(clickedHandle) n = self.mSelectedHandles.size() delIcon = QIcon(":images/16x16/edit-delete.png") delText = self.tr("Delete %n Node(s)", "", n) menu = QMenu() deleteNodesAction = menu.addAction(delIcon, delText) joinNodesAction = menu.addAction(self.tr("Join Nodes")) splitSegmentsAction = menu.addAction(self.tr("Split Segments")) Utils.setThemeIcon(deleteNodesAction, "edit-delete") joinNodesAction.setEnabled(n > 1) splitSegmentsAction.setEnabled(n > 1) deleteNodesAction.triggered.connect(self.deleteNodes) joinNodesAction.triggered.connect(self.joinNodes) splitSegmentsAction.triggered.connect(self.splitSegments) menu.exec(screenPos)
def __init__(self, workingDocument, rules, rulePath): ## # where to work in ## self.mMapDocument = workingDocument ## # the same as mMapDocument.map() ## self.mMapWork = None if workingDocument: self.mMapWork = workingDocument.map() ## # map containing the rules, usually different than mMapWork ## self.mMapRules = rules ## # This contains all added tilesets as pointers. # if rules use Tilesets which are not in the mMapWork they are added. # keep track of them, because we need to delete them afterwards, # when they still are unused # they will be added while setupTilesets(). ## self.mAddedTilesets = QVector() ## # description see: mAddedTilesets, just described by Strings ## self.mAddedTileLayers = QList() ## # Points to the tilelayer, which defines the inputregions. ## self.mLayerInputRegions = None ## # Points to the tilelayer, which defines the outputregions. ## self.mLayerOutputRegions = None ## # Contains all tilelayer pointers, which names begin with input* # It is sorted by index and name ## self.mInputRules = InputLayers() ## # List of Regions in mMapRules to know where the input rules are ## self.mRulesInput = QList() ## # List of regions in mMapRules to know where the output of a # rule is. # mRulesOutput[i] is the output of that rule, # which has the input at mRulesInput[i], meaning that mRulesInput # and mRulesOutput must match with the indexes. ## self.mRulesOutput = QList() ## # The inner set with layers to indexes is needed for translating # tile layers from mMapRules to mMapWork. # # The key is the pointer to the layer in the rulemap. The # pointer to the layer within the working map is not hardwired, but the # position in the layerlist, where it was found the last time. # This loosely bound pointer ensures we will get the right layer, since we # need to check before anyway, and it is still fast. # # The list is used to hold different translation tables # => one of the tables is chosen by chance, so randomness is available ## self.mLayerList = QList() ## # store the name of the processed rules file, to have detailed # error messages available ## self.mRulePath = rulePath ## # determines if all tiles in all touched layers should be deleted first. ## self.mDeleteTiles = False ## # This variable determines, how many overlapping tiles should be used. # The bigger the more area is remapped at an automapping operation. # This can lead to higher latency, but provides a better behavior on # interactive automapping. # It defaults to zero. ## self.mAutoMappingRadius = 0 ## # Determines if a rule is allowed to overlap it ## self.mNoOverlappingRules = False self.mTouchedObjectGroups = QSet() self.mWarning = QString() self.mTouchedTileLayers = QSet() self.mError = '' if (not self.setupRuleMapProperties()): return if (not self.setupRuleMapTileLayers()): return if (not self.setupRuleList()): return
class TilesetManager(QObject): mInstance = None ## # Emitted when a tileset's images have changed and views need updating. ## tilesetChanged = pyqtSignal(Tileset) ## # Emitted when any images of the tiles in the given \a tileset have # changed. This is used to trigger repaints for displaying tile # animations. ## repaintTileset = pyqtSignal(Tileset) ## # Constructor. Only used by the tileset manager it ## def __init__(self): super().__init__() ## # Stores the tilesets and maps them to the number of references. ## self.mTilesets = QMap() self.mChangedFiles = QSet() self.mWatcher = FileSystemWatcher(self) self.mAnimationDriver = TileAnimationDriver(self) self.mReloadTilesetsOnChange = False self.mChangedFilesTimer = QTimer() self.mWatcher.fileChanged.connect(self.fileChanged) self.mChangedFilesTimer.setInterval(500) self.mChangedFilesTimer.setSingleShot(True) self.mChangedFilesTimer.timeout.connect(self.fileChangedTimeout) self.mAnimationDriver.update.connect(self.advanceTileAnimations) ## # Destructor. ## def __del__(self): # Since all MapDocuments should be deleted first, we assert that there are # no remaining tileset references. self.mTilesets.size() == 0 ## # Requests the tileset manager. When the manager doesn't exist yet, it # will be created. ## def instance(): if (not TilesetManager.mInstance): TilesetManager.mInstance = TilesetManager() return TilesetManager.mInstance ## # Deletes the tileset manager instance, when it exists. ## def deleteInstance(): del TilesetManager.mInstance TilesetManager.mInstance = None def findTileset(self, arg): tp = type(arg) if tp in [QString, str]: ## # Searches for a tileset matching the given file name. # @return a tileset matching the given file name, or 0 if none exists ## fileName = arg for tileset in self.tilesets(): if (tileset.fileName() == fileName): return tileset return None elif tp==TilesetSpec: ## # Searches for a tileset matching the given specification. # @return a tileset matching the given specification, or 0 if none exists ## spec = arg for tileset in self.tilesets(): if (tileset.imageSource() == spec.imageSource and tileset.tileWidth() == spec.tileWidth and tileset.tileHeight() == spec.tileHeight and tileset.tileSpacing() == spec.tileSpacing and tileset.margin() == spec.margin): return tileset return None ## # Adds a tileset reference. This will make sure the tileset is watched for # changes and can be found using findTileset(). ## def addReference(self, tileset): if (self.mTilesets.contains(tileset)): self.mTilesets[tileset] += 1 else: self.mTilesets.insert(tileset, 1) imgSrc = tileset.imageSource() if (imgSrc != ''): self.mWatcher.addPath(imgSrc) ## # Removes a tileset reference. When the last reference has been removed, # the tileset is no longer watched for changes. ## def removeReference(self, tileset): if self.mTilesets[tileset]: self.mTilesets[tileset] -= 1 if (self.mTilesets[tileset] == 0): self.mTilesets.remove(tileset) if (tileset.imageSource()!=''): self.mWatcher.removePath(tileset.imageSource()) ## # Convenience method to add references to multiple tilesets. # @see addReference ## def addReferences(self, tilesets): for tileset in tilesets: self.addReference(tileset) ## # Convenience method to remove references from multiple tilesets. # @see removeReference ## def removeReferences(self, tilesets): for tileset in tilesets: self.removeReference(tileset) ## # Returns all currently available tilesets. ## def tilesets(self): return self.mTilesets.keys() ## # Forces a tileset to reload. ## def forceTilesetReload(self, tileset): if (not self.mTilesets.contains(tileset)): return fileName = tileset.imageSource() if (tileset.loadFromImage(fileName)): self.tilesetChanged.emit(tileset) ## # Sets whether tilesets are automatically reloaded when their tileset # image changes. ## def setReloadTilesetsOnChange(self, enabled): self.mReloadTilesetsOnChange = enabled # TODO: Clear the file system watcher when disabled def reloadTilesetsOnChange(self): return self.mReloadTilesetsOnChange ## # Sets whether tile animations are running. ## def setAnimateTiles(self, enabled): # TODO: Avoid running the driver when there are no animated tiles if (enabled): self.mAnimationDriver.start() else: self.mAnimationDriver.stop() def animateTiles(self): return self.mAnimationDriver.state() == QAbstractAnimation.Running def fileChanged(self, path): if (not self.mReloadTilesetsOnChange): return ## # Use a one-shot timer since GIMP (for example) seems to generate many # file changes during a save, and some of the intermediate attempts to # reload the tileset images actually fail (at least for .png files). ## self.mChangedFiles.insert(path) self.mChangedFilesTimer.start() def fileChangedTimeout(self): for tileset in self.tilesets(): fileName = tileset.imageSource() if (self.mChangedFiles.contains(fileName)): if (tileset.loadFromImage(fileName)): self.tilesetChanged.emit(tileset) self.mChangedFiles.clear() def advanceTileAnimations(self, ms): # TODO: This could be more optimal by keeping track of the list of # actually animated tiles for tileset in self.tilesets(): imageChanged = False for tile in tileset.tiles(): imageChanged |= tile.advanceAnimation(ms) if (imageChanged): self.repaintTileset.emit(tileset)
def selectedObjectItems(self): return QSet(self.mSelectedObjectItems)
class MapScene(QGraphicsScene): selectedObjectItemsChanged = pyqtSignal() ## # Constructor. ## def __init__(self, parent): super().__init__(parent) self.mMapDocument = None self.mSelectedTool = None self.mActiveTool = None self.mObjectSelectionItem = None self.mUnderMouse = False self.mCurrentModifiers = Qt.NoModifier, self.mDarkRectangle = QGraphicsRectItem() self.mDefaultBackgroundColor = Qt.darkGray self.mLayerItems = QVector() self.mObjectItems = QMap() self.mObjectLineWidth = 0.0 self.mSelectedObjectItems = QSet() self.mLastMousePos = QPointF() self.mShowTileObjectOutlines = False self.mHighlightCurrentLayer = False self.mGridVisible = False self.setBackgroundBrush(self.mDefaultBackgroundColor) tilesetManager = TilesetManager.instance() tilesetManager.tilesetChanged.connect(self.tilesetChanged) tilesetManager.repaintTileset.connect(self.tilesetChanged) prefs = preferences.Preferences.instance() prefs.showGridChanged.connect(self.setGridVisible) prefs.showTileObjectOutlinesChanged.connect( self.setShowTileObjectOutlines) prefs.objectTypesChanged.connect(self.syncAllObjectItems) prefs.highlightCurrentLayerChanged.connect( self.setHighlightCurrentLayer) prefs.gridColorChanged.connect(self.update) prefs.objectLineWidthChanged.connect(self.setObjectLineWidth) self.mDarkRectangle.setPen(QPen(Qt.NoPen)) self.mDarkRectangle.setBrush(Qt.black) self.mDarkRectangle.setOpacity(darkeningFactor) self.addItem(self.mDarkRectangle) self.mGridVisible = prefs.showGrid() self.mObjectLineWidth = prefs.objectLineWidth() self.mShowTileObjectOutlines = prefs.showTileObjectOutlines() self.mHighlightCurrentLayer = prefs.highlightCurrentLayer() # Install an event filter so that we can get key events on behalf of the # active tool without having to have the current focus. QCoreApplication.instance().installEventFilter(self) ## # Destructor. ## def __del__(self): if QCoreApplication.instance(): QCoreApplication.instance().removeEventFilter(self) ## # Returns the map document this scene is displaying. ## def mapDocument(self): return self.mMapDocument ## # Sets the map this scene displays. ## def setMapDocument(self, mapDocument): if (self.mMapDocument): self.mMapDocument.disconnect() if (not self.mSelectedObjectItems.isEmpty()): self.mSelectedObjectItems.clear() self.selectedObjectItemsChanged.emit() self.mMapDocument = mapDocument if (self.mMapDocument): renderer = self.mMapDocument.renderer() renderer.setObjectLineWidth(self.mObjectLineWidth) renderer.setFlag(RenderFlag.ShowTileObjectOutlines, self.mShowTileObjectOutlines) self.mMapDocument.mapChanged.connect(self.mapChanged) self.mMapDocument.regionChanged.connect(self.repaintRegion) self.mMapDocument.tileLayerDrawMarginsChanged.connect( self.tileLayerDrawMarginsChanged) self.mMapDocument.layerAdded.connect(self.layerAdded) self.mMapDocument.layerRemoved.connect(self.layerRemoved) self.mMapDocument.layerChanged.connect(self.layerChanged) self.mMapDocument.objectGroupChanged.connect( self.objectGroupChanged) self.mMapDocument.imageLayerChanged.connect(self.imageLayerChanged) self.mMapDocument.currentLayerIndexChanged.connect( self.currentLayerIndexChanged) self.mMapDocument.tilesetTileOffsetChanged.connect( self.tilesetTileOffsetChanged) self.mMapDocument.objectsInserted.connect(self.objectsInserted) self.mMapDocument.objectsRemoved.connect(self.objectsRemoved) self.mMapDocument.objectsChanged.connect(self.objectsChanged) self.mMapDocument.objectsIndexChanged.connect( self.objectsIndexChanged) self.mMapDocument.selectedObjectsChanged.connect( self.updateSelectedObjectItems) self.refreshScene() ## # Returns whether the tile grid is visible. ## def isGridVisible(self): return self.mGridVisible ## # Returns the set of selected map object items. ## def selectedObjectItems(self): return QSet(self.mSelectedObjectItems) ## # Sets the set of selected map object items. This translates to a call to # MapDocument.setSelectedObjects. ## def setSelectedObjectItems(self, items): # Inform the map document about the newly selected objects selectedObjects = QList() #selectedObjects.reserve(items.size()) for item in items: selectedObjects.append(item.mapObject()) self.mMapDocument.setSelectedObjects(selectedObjects) ## # Returns the MapObjectItem associated with the given \a mapObject. ## def itemForObject(self, object): return self.mObjectItems[object] ## # Enables the selected tool at this map scene. # Therefore it tells that tool, that this is the active map scene. ## def enableSelectedTool(self): if (not self.mSelectedTool or not self.mMapDocument): return self.mActiveTool = self.mSelectedTool self.mActiveTool.activate(self) self.mCurrentModifiers = QApplication.keyboardModifiers() if (self.mCurrentModifiers != Qt.NoModifier): self.mActiveTool.modifiersChanged(self.mCurrentModifiers) if (self.mUnderMouse): self.mActiveTool.mouseEntered() self.mActiveTool.mouseMoved(self.mLastMousePos, Qt.KeyboardModifiers()) def disableSelectedTool(self): if (not self.mActiveTool): return if (self.mUnderMouse): self.mActiveTool.mouseLeft() self.mActiveTool.deactivate(self) self.mActiveTool = None ## # Sets the currently selected tool. ## def setSelectedTool(self, tool): self.mSelectedTool = tool ## # QGraphicsScene.drawForeground override that draws the tile grid. ## def drawForeground(self, painter, rect): if (not self.mMapDocument or not self.mGridVisible): return offset = QPointF() # Take into account the offset of the current layer layer = self.mMapDocument.currentLayer() if layer: offset = layer.offset() painter.translate(offset) prefs = preferences.Preferences.instance() self.mMapDocument.renderer().drawGrid(painter, rect.translated(-offset), prefs.gridColor()) ## # Override for handling enter and leave events. ## def event(self, event): x = event.type() if x == QEvent.Enter: self.mUnderMouse = True if (self.mActiveTool): self.mActiveTool.mouseEntered() elif x == QEvent.Leave: self.mUnderMouse = False if (self.mActiveTool): self.mActiveTool.mouseLeft() else: pass return super().event(event) def keyPressEvent(self, event): if (self.mActiveTool): self.mActiveTool.keyPressed(event) if (not (self.mActiveTool and event.isAccepted())): super().keyPressEvent(event) def mouseMoveEvent(self, mouseEvent): self.mLastMousePos = mouseEvent.scenePos() if (not self.mMapDocument): return super().mouseMoveEvent(mouseEvent) if (mouseEvent.isAccepted()): return if (self.mActiveTool): self.mActiveTool.mouseMoved(mouseEvent.scenePos(), mouseEvent.modifiers()) mouseEvent.accept() def mousePressEvent(self, mouseEvent): super().mousePressEvent(mouseEvent) if (mouseEvent.isAccepted()): return if (self.mActiveTool): mouseEvent.accept() self.mActiveTool.mousePressed(mouseEvent) def mouseReleaseEvent(self, mouseEvent): super().mouseReleaseEvent(mouseEvent) if (mouseEvent.isAccepted()): return if (self.mActiveTool): mouseEvent.accept() self.mActiveTool.mouseReleased(mouseEvent) ## # Override to ignore drag enter events. ## def dragEnterEvent(self, event): event.ignore() ## # Sets whether the tile grid is visible. ## def setGridVisible(self, visible): if (self.mGridVisible == visible): return self.mGridVisible = visible self.update() def setObjectLineWidth(self, lineWidth): if (self.mObjectLineWidth == lineWidth): return self.mObjectLineWidth = lineWidth if (self.mMapDocument): self.mMapDocument.renderer().setObjectLineWidth(lineWidth) # Changing the line width can change the size of the object items if (not self.mObjectItems.isEmpty()): for item in self.mObjectItems: item[1].syncWithMapObject() self.update() def setShowTileObjectOutlines(self, enabled): if (self.mShowTileObjectOutlines == enabled): return self.mShowTileObjectOutlines = enabled if (self.mMapDocument): self.mMapDocument.renderer().setFlag( RenderFlag.ShowTileObjectOutlines, enabled) if (not self.mObjectItems.isEmpty()): self.update() ## # Sets whether the current layer should be highlighted. ## def setHighlightCurrentLayer(self, highlightCurrentLayer): if (self.mHighlightCurrentLayer == highlightCurrentLayer): return self.mHighlightCurrentLayer = highlightCurrentLayer self.updateCurrentLayerHighlight() ## # Refreshes the map scene. ## def refreshScene(self): self.mLayerItems.clear() self.mObjectItems.clear() self.removeItem(self.mDarkRectangle) self.clear() self.addItem(self.mDarkRectangle) if (not self.mMapDocument): self.setSceneRect(QRectF()) return self.updateSceneRect() map = self.mMapDocument.map() self.mLayerItems.resize(map.layerCount()) if (map.backgroundColor().isValid()): self.setBackgroundBrush(map.backgroundColor()) else: self.setBackgroundBrush(self.mDefaultBackgroundColor) layerIndex = 0 for layer in map.layers(): layerItem = self.createLayerItem(layer) layerItem.setZValue(layerIndex) self.addItem(layerItem) self.mLayerItems[layerIndex] = layerItem layerIndex += 1 tileSelectionItem = TileSelectionItem(self.mMapDocument) tileSelectionItem.setZValue(10000 - 2) self.addItem(tileSelectionItem) self.mObjectSelectionItem = ObjectSelectionItem(self.mMapDocument) self.mObjectSelectionItem.setZValue(10000 - 1) self.addItem(self.mObjectSelectionItem) self.updateCurrentLayerHighlight() ## # Repaints the specified region. The region is in tile coordinates. ## def repaintRegion(self, region, layer): renderer = self.mMapDocument.renderer() margins = self.mMapDocument.map().drawMargins() for r in region.rects(): boundingRect = QRectF(renderer.boundingRect(r)) self.update( QRectF( renderer.boundingRect(r).adjusted(-margins.left(), -margins.top(), margins.right(), margins.bottom()))) boundingRect.translate(layer.offset()) self.update(boundingRect) def currentLayerIndexChanged(self): self.updateCurrentLayerHighlight() # New layer may have a different offset, affecting the grid if self.mGridVisible: self.update() ## # Adapts the scene, layers and objects to new map size, orientation or # background color. ## def mapChanged(self): self.updateSceneRect() for item in self.mLayerItems: tli = item if type(tli) == TileLayerItem: tli.syncWithTileLayer() for item in self.mObjectItems.values(): item.syncWithMapObject() map = self.mMapDocument.map() if (map.backgroundColor().isValid()): self.setBackgroundBrush(map.backgroundColor()) else: self.setBackgroundBrush(self.mDefaultBackgroundColor) def tilesetChanged(self, tileset): if (not self.mMapDocument): return if (contains(self.mMapDocument.map().tilesets(), tileset)): self.update() def tileLayerDrawMarginsChanged(self, tileLayer): index = self.mMapDocument.map().layers().indexOf(tileLayer) item = self.mLayerItems.at(index) item.syncWithTileLayer() def layerAdded(self, index): layer = self.mMapDocument.map().layerAt(index) layerItem = self.createLayerItem(layer) self.addItem(layerItem) self.mLayerItems.insert(index, layerItem) z = 0 for item in self.mLayerItems: item.setZValue(z) z += 1 def layerRemoved(self, index): self.mLayerItems.remove(index) ## # A layer has changed. This can mean that the layer visibility, opacity or # offset changed. ## def layerChanged(self, index): layer = self.mMapDocument.map().layerAt(index) layerItem = self.mLayerItems.at(index) layerItem.setVisible(layer.isVisible()) multiplier = 1 if (self.mHighlightCurrentLayer and self.mMapDocument.currentLayerIndex() < index): multiplier = opacityFactor layerItem.setOpacity(layer.opacity() * multiplier) layerItem.setPos(layer.offset()) # Layer offset may have changed, affecting the scene rect and grid self.updateSceneRect() if self.mGridVisible: self.update() ## # When an object group has changed it may mean its color or drawing order # changed, which affects all its objects. ## def objectGroupChanged(self, objectGroup): self.objectsChanged(objectGroup.objects()) self.objectsIndexChanged(objectGroup, 0, objectGroup.objectCount() - 1) ## # When an image layer has changed, it may change size and it may look # differently. ## def imageLayerChanged(self, imageLayer): index = self.mMapDocument.map().layers().indexOf(imageLayer) item = self.mLayerItems.at(index) item.syncWithImageLayer() item.update() ## # When the tile offset of a tileset has changed, it can affect the bounding # rect of all tile layers and tile objects. It also requires a full repaint. ## def tilesetTileOffsetChanged(self, tileset): self.update() for item in self.mLayerItems: tli = item if type(tli) == TileLayerItem: tli.syncWithTileLayer() for item in self.mObjectItems: cell = item.mapObject().cell() if (not cell.isEmpty() and cell.tile.tileset() == tileset): item.syncWithMapObject() ## # Inserts map object items for the given objects. ## def objectsInserted(self, objectGroup, first, last): ogItem = None # Find the object group item for the object group for item in self.mLayerItems: ogi = item if type(ogi) == ObjectGroupItem: if (ogi.objectGroup() == objectGroup): ogItem = ogi break drawOrder = objectGroup.drawOrder() for i in range(first, last + 1): object = objectGroup.objectAt(i) item = MapObjectItem(object, self.mMapDocument, ogItem) if (drawOrder == ObjectGroup.DrawOrder.TopDownOrder): item.setZValue(item.y()) else: item.setZValue(i) self.mObjectItems.insert(object, item) ## # Removes the map object items related to the given objects. ## def objectsRemoved(self, objects): for o in objects: i = self.mObjectItems.find(o) self.mSelectedObjectItems.remove(i) # python would not force delete QGraphicsItem self.removeItem(i) self.mObjectItems.erase(o) ## # Updates the map object items related to the given objects. ## def objectsChanged(self, objects): for object in objects: item = self.itemForObject(object) item.syncWithMapObject() ## # Updates the Z value of the objects when appropriate. ## def objectsIndexChanged(self, objectGroup, first, last): if (objectGroup.drawOrder() != ObjectGroup.DrawOrder.IndexOrder): return for i in range(first, last + 1): item = self.itemForObject(objectGroup.objectAt(i)) item.setZValue(i) def updateSelectedObjectItems(self): objects = self.mMapDocument.selectedObjects() items = QSet() for object in objects: item = self.itemForObject(object) if item: items.insert(item) self.mSelectedObjectItems = items self.selectedObjectItemsChanged.emit() def syncAllObjectItems(self): for item in self.mObjectItems: item.syncWithMapObject() def createLayerItem(self, layer): layerItem = None tl = layer.asTileLayer() if tl: layerItem = TileLayerItem(tl, self.mMapDocument) else: og = layer.asObjectGroup() if og: drawOrder = og.drawOrder() ogItem = ObjectGroupItem(og) objectIndex = 0 for object in og.objects(): item = MapObjectItem(object, self.mMapDocument, ogItem) if (drawOrder == ObjectGroup.DrawOrder.TopDownOrder): item.setZValue(item.y()) else: item.setZValue(objectIndex) self.mObjectItems.insert(object, item) objectIndex += 1 layerItem = ogItem else: il = layer.asImageLayer() if il: layerItem = ImageLayerItem(il, self.mMapDocument) layerItem.setVisible(layer.isVisible()) return layerItem def updateSceneRect(self): mapSize = self.mMapDocument.renderer().mapSize() sceneRect = QRectF(0, 0, mapSize.width(), mapSize.height()) margins = self.mMapDocument.map().computeLayerOffsetMargins() sceneRect.adjust(-margins.left(), -margins.top(), margins.right(), margins.bottom()) self.setSceneRect(sceneRect) self.mDarkRectangle.setRect(sceneRect) def updateCurrentLayerHighlight(self): if (not self.mMapDocument): return currentLayerIndex = self.mMapDocument.currentLayerIndex() if (not self.mHighlightCurrentLayer or currentLayerIndex == -1): self.mDarkRectangle.setVisible(False) # Restore opacity for all layers for i in range(self.mLayerItems.size()): layer = self.mMapDocument.map().layerAt(i) self.mLayerItems.at(i).setOpacity(layer.opacity()) return # Darken layers below the current layer self.mDarkRectangle.setZValue(currentLayerIndex - 0.5) self.mDarkRectangle.setVisible(True) # Set layers above the current layer to half opacity for i in range(1, self.mLayerItems.size()): layer = self.mMapDocument.map().layerAt(i) if currentLayerIndex < i: _x = opacityFactor else: _x = 1 multiplier = _x self.mLayerItems.at(i).setOpacity(layer.opacity() * multiplier) def eventFilter(self, object, event): x = event.type() if x == QEvent.KeyPress or x == QEvent.KeyRelease: keyEvent = event newModifiers = keyEvent.modifiers() if (self.mActiveTool and newModifiers != self.mCurrentModifiers): self.mActiveTool.modifiersChanged(newModifiers) self.mCurrentModifiers = newModifiers else: pass return False
class TilesetManager(QObject): mInstance = None ## # Emitted when a tileset's images have changed and views need updating. ## tilesetChanged = pyqtSignal(Tileset) ## # Emitted when any images of the tiles in the given \a tileset have # changed. This is used to trigger repaints for displaying tile # animations. ## repaintTileset = pyqtSignal(Tileset) ## # Constructor. Only used by the tileset manager it ## def __init__(self): super().__init__() ## # Stores the tilesets and maps them to the number of references. ## self.mTilesets = QMap() self.mChangedFiles = QSet() self.mWatcher = FileSystemWatcher(self) self.mAnimationDriver = TileAnimationDriver(self) self.mReloadTilesetsOnChange = False self.mChangedFilesTimer = QTimer() self.mWatcher.fileChanged.connect(self.fileChanged) self.mChangedFilesTimer.setInterval(500) self.mChangedFilesTimer.setSingleShot(True) self.mChangedFilesTimer.timeout.connect(self.fileChangedTimeout) self.mAnimationDriver.update.connect(self.advanceTileAnimations) ## # Destructor. ## def __del__(self): # Since all MapDocuments should be deleted first, we assert that there are # no remaining tileset references. self.mTilesets.size() == 0 ## # Requests the tileset manager. When the manager doesn't exist yet, it # will be created. ## def instance(): if (not TilesetManager.mInstance): TilesetManager.mInstance = TilesetManager() return TilesetManager.mInstance ## # Deletes the tileset manager instance, when it exists. ## def deleteInstance(): del TilesetManager.mInstance TilesetManager.mInstance = None def findTileset(self, arg): tp = type(arg) if tp in [QString, str]: ## # Searches for a tileset matching the given file name. # @return a tileset matching the given file name, or 0 if none exists ## fileName = arg for tileset in self.tilesets(): if (tileset.fileName() == fileName): return tileset return None elif tp == TilesetSpec: ## # Searches for a tileset matching the given specification. # @return a tileset matching the given specification, or 0 if none exists ## spec = arg for tileset in self.tilesets(): if (tileset.imageSource() == spec.imageSource and tileset.tileWidth() == spec.tileWidth and tileset.tileHeight() == spec.tileHeight and tileset.tileSpacing() == spec.tileSpacing and tileset.margin() == spec.margin): return tileset return None ## # Adds a tileset reference. This will make sure the tileset is watched for # changes and can be found using findTileset(). ## def addReference(self, tileset): if (self.mTilesets.contains(tileset)): self.mTilesets[tileset] += 1 else: self.mTilesets.insert(tileset, 1) imgSrc = tileset.imageSource() if (imgSrc != ''): self.mWatcher.addPath(imgSrc) ## # Removes a tileset reference. When the last reference has been removed, # the tileset is no longer watched for changes. ## def removeReference(self, tileset): if self.mTilesets[tileset]: self.mTilesets[tileset] -= 1 if (self.mTilesets[tileset] == 0): self.mTilesets.remove(tileset) if (tileset.imageSource() != ''): self.mWatcher.removePath(tileset.imageSource()) ## # Convenience method to add references to multiple tilesets. # @see addReference ## def addReferences(self, tilesets): for tileset in tilesets: self.addReference(tileset) ## # Convenience method to remove references from multiple tilesets. # @see removeReference ## def removeReferences(self, tilesets): for tileset in tilesets: self.removeReference(tileset) ## # Returns all currently available tilesets. ## def tilesets(self): return self.mTilesets.keys() ## # Forces a tileset to reload. ## def forceTilesetReload(self, tileset): if (not self.mTilesets.contains(tileset)): return fileName = tileset.imageSource() if (tileset.loadFromImage(fileName)): self.tilesetChanged.emit(tileset) ## # Sets whether tilesets are automatically reloaded when their tileset # image changes. ## def setReloadTilesetsOnChange(self, enabled): self.mReloadTilesetsOnChange = enabled # TODO: Clear the file system watcher when disabled def reloadTilesetsOnChange(self): return self.mReloadTilesetsOnChange ## # Sets whether tile animations are running. ## def setAnimateTiles(self, enabled): # TODO: Avoid running the driver when there are no animated tiles if (enabled): self.mAnimationDriver.start() else: self.mAnimationDriver.stop() def animateTiles(self): return self.mAnimationDriver.state() == QAbstractAnimation.Running def fileChanged(self, path): if (not self.mReloadTilesetsOnChange): return ## # Use a one-shot timer since GIMP (for example) seems to generate many # file changes during a save, and some of the intermediate attempts to # reload the tileset images actually fail (at least for .png files). ## self.mChangedFiles.insert(path) self.mChangedFilesTimer.start() def fileChangedTimeout(self): for tileset in self.tilesets(): fileName = tileset.imageSource() if (self.mChangedFiles.contains(fileName)): if (tileset.loadFromImage(fileName)): self.tilesetChanged.emit(tileset) self.mChangedFiles.clear() def advanceTileAnimations(self, ms): # TODO: This could be more optimal by keeping track of the list of # actually animated tiles for tileset in self.tilesets(): imageChanged = False for tile in tileset.tiles(): imageChanged |= tile.advanceAnimation(ms) if (imageChanged): self.repaintTileset.emit(tileset)
class MapScene(QGraphicsScene): selectedObjectItemsChanged = pyqtSignal() ## # Constructor. ## def __init__(self, parent): super().__init__(parent) self.mMapDocument = None self.mSelectedTool = None self.mActiveTool = None self.mObjectSelectionItem = None self.mUnderMouse = False self.mCurrentModifiers = Qt.NoModifier, self.mDarkRectangle = QGraphicsRectItem() self.mDefaultBackgroundColor = Qt.darkGray self.mLayerItems = QVector() self.mObjectItems = QMap() self.mObjectLineWidth = 0.0 self.mSelectedObjectItems = QSet() self.mLastMousePos = QPointF() self.mShowTileObjectOutlines = False self.mHighlightCurrentLayer = False self.mGridVisible = False self.setBackgroundBrush(self.mDefaultBackgroundColor) tilesetManager = TilesetManager.instance() tilesetManager.tilesetChanged.connect(self.tilesetChanged) tilesetManager.repaintTileset.connect(self.tilesetChanged) prefs = preferences.Preferences.instance() prefs.showGridChanged.connect(self.setGridVisible) prefs.showTileObjectOutlinesChanged.connect(self.setShowTileObjectOutlines) prefs.objectTypesChanged.connect(self.syncAllObjectItems) prefs.highlightCurrentLayerChanged.connect(self.setHighlightCurrentLayer) prefs.gridColorChanged.connect(self.update) prefs.objectLineWidthChanged.connect(self.setObjectLineWidth) self.mDarkRectangle.setPen(QPen(Qt.NoPen)) self.mDarkRectangle.setBrush(Qt.black) self.mDarkRectangle.setOpacity(darkeningFactor) self.addItem(self.mDarkRectangle) self.mGridVisible = prefs.showGrid() self.mObjectLineWidth = prefs.objectLineWidth() self.mShowTileObjectOutlines = prefs.showTileObjectOutlines() self.mHighlightCurrentLayer = prefs.highlightCurrentLayer() # Install an event filter so that we can get key events on behalf of the # active tool without having to have the current focus. QCoreApplication.instance().installEventFilter(self) ## # Destructor. ## def __del__(self): if QCoreApplication.instance(): QCoreApplication.instance().removeEventFilter(self) ## # Returns the map document this scene is displaying. ## def mapDocument(self): return self.mMapDocument ## # Sets the map this scene displays. ## def setMapDocument(self, mapDocument): if (self.mMapDocument): self.mMapDocument.disconnect() if (not self.mSelectedObjectItems.isEmpty()): self.mSelectedObjectItems.clear() self.selectedObjectItemsChanged.emit() self.mMapDocument = mapDocument if (self.mMapDocument): renderer = self.mMapDocument.renderer() renderer.setObjectLineWidth(self.mObjectLineWidth) renderer.setFlag(RenderFlag.ShowTileObjectOutlines, self.mShowTileObjectOutlines) self.mMapDocument.mapChanged.connect(self.mapChanged) self.mMapDocument.regionChanged.connect(self.repaintRegion) self.mMapDocument.tileLayerDrawMarginsChanged.connect(self.tileLayerDrawMarginsChanged) self.mMapDocument.layerAdded.connect(self.layerAdded) self.mMapDocument.layerRemoved.connect(self.layerRemoved) self.mMapDocument.layerChanged.connect(self.layerChanged) self.mMapDocument.objectGroupChanged.connect(self.objectGroupChanged) self.mMapDocument.imageLayerChanged.connect(self.imageLayerChanged) self.mMapDocument.currentLayerIndexChanged.connect(self.currentLayerIndexChanged) self.mMapDocument.tilesetTileOffsetChanged.connect(self.tilesetTileOffsetChanged) self.mMapDocument.objectsInserted.connect(self.objectsInserted) self.mMapDocument.objectsRemoved.connect(self.objectsRemoved) self.mMapDocument.objectsChanged.connect(self.objectsChanged) self.mMapDocument.objectsIndexChanged.connect(self.objectsIndexChanged) self.mMapDocument.selectedObjectsChanged.connect(self.updateSelectedObjectItems) self.refreshScene() ## # Returns whether the tile grid is visible. ## def isGridVisible(self): return self.mGridVisible ## # Returns the set of selected map object items. ## def selectedObjectItems(self): return QSet(self.mSelectedObjectItems) ## # Sets the set of selected map object items. This translates to a call to # MapDocument.setSelectedObjects. ## def setSelectedObjectItems(self, items): # Inform the map document about the newly selected objects selectedObjects = QList() #selectedObjects.reserve(items.size()) for item in items: selectedObjects.append(item.mapObject()) self.mMapDocument.setSelectedObjects(selectedObjects) ## # Returns the MapObjectItem associated with the given \a mapObject. ## def itemForObject(self, object): return self.mObjectItems[object] ## # Enables the selected tool at this map scene. # Therefore it tells that tool, that this is the active map scene. ## def enableSelectedTool(self): if (not self.mSelectedTool or not self.mMapDocument): return self.mActiveTool = self.mSelectedTool self.mActiveTool.activate(self) self.mCurrentModifiers = QApplication.keyboardModifiers() if (self.mCurrentModifiers != Qt.NoModifier): self.mActiveTool.modifiersChanged(self.mCurrentModifiers) if (self.mUnderMouse): self.mActiveTool.mouseEntered() self.mActiveTool.mouseMoved(self.mLastMousePos, Qt.KeyboardModifiers()) def disableSelectedTool(self): if (not self.mActiveTool): return if (self.mUnderMouse): self.mActiveTool.mouseLeft() self.mActiveTool.deactivate(self) self.mActiveTool = None ## # Sets the currently selected tool. ## def setSelectedTool(self, tool): self.mSelectedTool = tool ## # QGraphicsScene.drawForeground override that draws the tile grid. ## def drawForeground(self, painter, rect): if (not self.mMapDocument or not self.mGridVisible): return offset = QPointF() # Take into account the offset of the current layer layer = self.mMapDocument.currentLayer() if layer: offset = layer.offset() painter.translate(offset) prefs = preferences.Preferences.instance() self.mMapDocument.renderer().drawGrid(painter, rect.translated(-offset), prefs.gridColor()) ## # Override for handling enter and leave events. ## def event(self, event): x = event.type() if x==QEvent.Enter: self.mUnderMouse = True if (self.mActiveTool): self.mActiveTool.mouseEntered() elif x==QEvent.Leave: self.mUnderMouse = False if (self.mActiveTool): self.mActiveTool.mouseLeft() else: pass return super().event(event) def keyPressEvent(self, event): if (self.mActiveTool): self.mActiveTool.keyPressed(event) if (not (self.mActiveTool and event.isAccepted())): super().keyPressEvent(event) def mouseMoveEvent(self, mouseEvent): self.mLastMousePos = mouseEvent.scenePos() if (not self.mMapDocument): return super().mouseMoveEvent(mouseEvent) if (mouseEvent.isAccepted()): return if (self.mActiveTool): self.mActiveTool.mouseMoved(mouseEvent.scenePos(), mouseEvent.modifiers()) mouseEvent.accept() def mousePressEvent(self, mouseEvent): super().mousePressEvent(mouseEvent) if (mouseEvent.isAccepted()): return if (self.mActiveTool): mouseEvent.accept() self.mActiveTool.mousePressed(mouseEvent) def mouseReleaseEvent(self, mouseEvent): super().mouseReleaseEvent(mouseEvent) if (mouseEvent.isAccepted()): return if (self.mActiveTool): mouseEvent.accept() self.mActiveTool.mouseReleased(mouseEvent) ## # Override to ignore drag enter events. ## def dragEnterEvent(self, event): event.ignore() ## # Sets whether the tile grid is visible. ## def setGridVisible(self, visible): if (self.mGridVisible == visible): return self.mGridVisible = visible self.update() def setObjectLineWidth(self, lineWidth): if (self.mObjectLineWidth == lineWidth): return self.mObjectLineWidth = lineWidth if (self.mMapDocument): self.mMapDocument.renderer().setObjectLineWidth(lineWidth) # Changing the line width can change the size of the object items if (not self.mObjectItems.isEmpty()): for item in self.mObjectItems: item[1].syncWithMapObject() self.update() def setShowTileObjectOutlines(self, enabled): if (self.mShowTileObjectOutlines == enabled): return self.mShowTileObjectOutlines = enabled if (self.mMapDocument): self.mMapDocument.renderer().setFlag(RenderFlag.ShowTileObjectOutlines, enabled) if (not self.mObjectItems.isEmpty()): self.update() ## # Sets whether the current layer should be highlighted. ## def setHighlightCurrentLayer(self, highlightCurrentLayer): if (self.mHighlightCurrentLayer == highlightCurrentLayer): return self.mHighlightCurrentLayer = highlightCurrentLayer self.updateCurrentLayerHighlight() ## # Refreshes the map scene. ## def refreshScene(self): self.mLayerItems.clear() self.mObjectItems.clear() self.removeItem(self.mDarkRectangle) self.clear() self.addItem(self.mDarkRectangle) if (not self.mMapDocument): self.setSceneRect(QRectF()) return self.updateSceneRect() map = self.mMapDocument.map() self.mLayerItems.resize(map.layerCount()) if (map.backgroundColor().isValid()): self.setBackgroundBrush(map.backgroundColor()) else: self.setBackgroundBrush(self.mDefaultBackgroundColor) layerIndex = 0 for layer in map.layers(): layerItem = self.createLayerItem(layer) layerItem.setZValue(layerIndex) self.addItem(layerItem) self.mLayerItems[layerIndex] = layerItem layerIndex += 1 tileSelectionItem = TileSelectionItem(self.mMapDocument) tileSelectionItem.setZValue(10000 - 2) self.addItem(tileSelectionItem) self.mObjectSelectionItem = ObjectSelectionItem(self.mMapDocument) self.mObjectSelectionItem.setZValue(10000 - 1) self.addItem(self.mObjectSelectionItem) self.updateCurrentLayerHighlight() ## # Repaints the specified region. The region is in tile coordinates. ## def repaintRegion(self, region, layer): renderer = self.mMapDocument.renderer() margins = self.mMapDocument.map().drawMargins() for r in region.rects(): boundingRect = QRectF(renderer.boundingRect(r)) self.update(QRectF(renderer.boundingRect(r).adjusted(-margins.left(), -margins.top(), margins.right(), margins.bottom()))) boundingRect.translate(layer.offset()) self.update(boundingRect) def currentLayerIndexChanged(self): self.updateCurrentLayerHighlight() # New layer may have a different offset, affecting the grid if self.mGridVisible: self.update() ## # Adapts the scene, layers and objects to new map size, orientation or # background color. ## def mapChanged(self): self.updateSceneRect() for item in self.mLayerItems: tli = item if type(tli) == TileLayerItem: tli.syncWithTileLayer() for item in self.mObjectItems.values(): item.syncWithMapObject() map = self.mMapDocument.map() if (map.backgroundColor().isValid()): self.setBackgroundBrush(map.backgroundColor()) else: self.setBackgroundBrush(self.mDefaultBackgroundColor) def tilesetChanged(self, tileset): if (not self.mMapDocument): return if (contains(self.mMapDocument.map().tilesets(), tileset)): self.update() def tileLayerDrawMarginsChanged(self, tileLayer): index = self.mMapDocument.map().layers().indexOf(tileLayer) item = self.mLayerItems.at(index) item.syncWithTileLayer() def layerAdded(self, index): layer = self.mMapDocument.map().layerAt(index) layerItem = self.createLayerItem(layer) self.addItem(layerItem) self.mLayerItems.insert(index, layerItem) z = 0 for item in self.mLayerItems: item.setZValue(z) z += 1 def layerRemoved(self, index): self.mLayerItems.remove(index) ## # A layer has changed. This can mean that the layer visibility, opacity or # offset changed. ## def layerChanged(self, index): layer = self.mMapDocument.map().layerAt(index) layerItem = self.mLayerItems.at(index) layerItem.setVisible(layer.isVisible()) multiplier = 1 if (self.mHighlightCurrentLayer and self.mMapDocument.currentLayerIndex() < index): multiplier = opacityFactor layerItem.setOpacity(layer.opacity() * multiplier) layerItem.setPos(layer.offset()) # Layer offset may have changed, affecting the scene rect and grid self.updateSceneRect() if self.mGridVisible: self.update() ## # When an object group has changed it may mean its color or drawing order # changed, which affects all its objects. ## def objectGroupChanged(self, objectGroup): self.objectsChanged(objectGroup.objects()) self.objectsIndexChanged(objectGroup, 0, objectGroup.objectCount() - 1) ## # When an image layer has changed, it may change size and it may look # differently. ## def imageLayerChanged(self, imageLayer): index = self.mMapDocument.map().layers().indexOf(imageLayer) item = self.mLayerItems.at(index) item.syncWithImageLayer() item.update() ## # When the tile offset of a tileset has changed, it can affect the bounding # rect of all tile layers and tile objects. It also requires a full repaint. ## def tilesetTileOffsetChanged(self, tileset): self.update() for item in self.mLayerItems: tli = item if type(tli) == TileLayerItem: tli.syncWithTileLayer() for item in self.mObjectItems: cell = item.mapObject().cell() if (not cell.isEmpty() and cell.tile.tileset() == tileset): item.syncWithMapObject() ## # Inserts map object items for the given objects. ## def objectsInserted(self, objectGroup, first, last): ogItem = None # Find the object group item for the object group for item in self.mLayerItems: ogi = item if type(ogi)==ObjectGroupItem: if (ogi.objectGroup() == objectGroup): ogItem = ogi break drawOrder = objectGroup.drawOrder() for i in range(first, last+1): object = objectGroup.objectAt(i) item = MapObjectItem(object, self.mMapDocument, ogItem) if (drawOrder == ObjectGroup.DrawOrder.TopDownOrder): item.setZValue(item.y()) else: item.setZValue(i) self.mObjectItems.insert(object, item) ## # Removes the map object items related to the given objects. ## def objectsRemoved(self, objects): for o in objects: i = self.mObjectItems.find(o) self.mSelectedObjectItems.remove(i) # python would not force delete QGraphicsItem self.removeItem(i) self.mObjectItems.erase(o) ## # Updates the map object items related to the given objects. ## def objectsChanged(self, objects): for object in objects: item = self.itemForObject(object) item.syncWithMapObject() ## # Updates the Z value of the objects when appropriate. ## def objectsIndexChanged(self, objectGroup, first, last): if (objectGroup.drawOrder() != ObjectGroup.DrawOrder.IndexOrder): return for i in range(first, last+1): item = self.itemForObject(objectGroup.objectAt(i)) item.setZValue(i) def updateSelectedObjectItems(self): objects = self.mMapDocument.selectedObjects() items = QSet() for object in objects: item = self.itemForObject(object) if item: items.insert(item) self.mSelectedObjectItems = items self.selectedObjectItemsChanged.emit() def syncAllObjectItems(self): for item in self.mObjectItems: item.syncWithMapObject() def createLayerItem(self, layer): layerItem = None tl = layer.asTileLayer() if tl: layerItem = TileLayerItem(tl, self.mMapDocument) else: og = layer.asObjectGroup() if og: drawOrder = og.drawOrder() ogItem = ObjectGroupItem(og) objectIndex = 0 for object in og.objects(): item = MapObjectItem(object, self.mMapDocument, ogItem) if (drawOrder == ObjectGroup.DrawOrder.TopDownOrder): item.setZValue(item.y()) else: item.setZValue(objectIndex) self.mObjectItems.insert(object, item) objectIndex += 1 layerItem = ogItem else: il = layer.asImageLayer() if il: layerItem = ImageLayerItem(il, self.mMapDocument) layerItem.setVisible(layer.isVisible()) return layerItem def updateSceneRect(self): mapSize = self.mMapDocument.renderer().mapSize() sceneRect = QRectF(0, 0, mapSize.width(), mapSize.height()) margins = self.mMapDocument.map().computeLayerOffsetMargins() sceneRect.adjust(-margins.left(), -margins.top(), margins.right(), margins.bottom()) self.setSceneRect(sceneRect) self.mDarkRectangle.setRect(sceneRect) def updateCurrentLayerHighlight(self): if (not self.mMapDocument): return currentLayerIndex = self.mMapDocument.currentLayerIndex() if (not self.mHighlightCurrentLayer or currentLayerIndex == -1): self.mDarkRectangle.setVisible(False) # Restore opacity for all layers for i in range(self.mLayerItems.size()): layer = self.mMapDocument.map().layerAt(i) self.mLayerItems.at(i).setOpacity(layer.opacity()) return # Darken layers below the current layer self.mDarkRectangle.setZValue(currentLayerIndex - 0.5) self.mDarkRectangle.setVisible(True) # Set layers above the current layer to half opacity for i in range(1, self.mLayerItems.size()): layer = self.mMapDocument.map().layerAt(i) if currentLayerIndex < i: _x = opacityFactor else: _x = 1 multiplier = _x self.mLayerItems.at(i).setOpacity(layer.opacity() * multiplier) def eventFilter(self, object, event): x = event.type() if x==QEvent.KeyPress or x==QEvent.KeyRelease: keyEvent = event newModifiers = keyEvent.modifiers() if (self.mActiveTool and newModifiers != self.mCurrentModifiers): self.mActiveTool.modifiersChanged(newModifiers) self.mCurrentModifiers = newModifiers else: pass return False
class AutoMapper(QObject): ## # Constructs an AutoMapper. # All data structures, which only rely on the rules map are setup # here. # # @param workingDocument: the map to work on. # @param rules: The rule map which should be used for automapping # @param rulePath: The filepath to the rule map. ## def __init__(self, workingDocument, rules, rulePath): ## # where to work in ## self.mMapDocument = workingDocument ## # the same as mMapDocument.map() ## self.mMapWork = None if workingDocument: self.mMapWork = workingDocument.map() ## # map containing the rules, usually different than mMapWork ## self.mMapRules = rules ## # This contains all added tilesets as pointers. # if rules use Tilesets which are not in the mMapWork they are added. # keep track of them, because we need to delete them afterwards, # when they still are unused # they will be added while setupTilesets(). ## self.mAddedTilesets = QVector() ## # description see: mAddedTilesets, just described by Strings ## self.mAddedTileLayers = QList() ## # Points to the tilelayer, which defines the inputregions. ## self.mLayerInputRegions = None ## # Points to the tilelayer, which defines the outputregions. ## self.mLayerOutputRegions = None ## # Contains all tilelayer pointers, which names begin with input* # It is sorted by index and name ## self.mInputRules = InputLayers() ## # List of Regions in mMapRules to know where the input rules are ## self.mRulesInput = QList() ## # List of regions in mMapRules to know where the output of a # rule is. # mRulesOutput[i] is the output of that rule, # which has the input at mRulesInput[i], meaning that mRulesInput # and mRulesOutput must match with the indexes. ## self.mRulesOutput = QList() ## # The inner set with layers to indexes is needed for translating # tile layers from mMapRules to mMapWork. # # The key is the pointer to the layer in the rulemap. The # pointer to the layer within the working map is not hardwired, but the # position in the layerlist, where it was found the last time. # This loosely bound pointer ensures we will get the right layer, since we # need to check before anyway, and it is still fast. # # The list is used to hold different translation tables # => one of the tables is chosen by chance, so randomness is available ## self.mLayerList = QList() ## # store the name of the processed rules file, to have detailed # error messages available ## self.mRulePath = rulePath ## # determines if all tiles in all touched layers should be deleted first. ## self.mDeleteTiles = False ## # This variable determines, how many overlapping tiles should be used. # The bigger the more area is remapped at an automapping operation. # This can lead to higher latency, but provides a better behavior on # interactive automapping. # It defaults to zero. ## self.mAutoMappingRadius = 0 ## # Determines if a rule is allowed to overlap it ## self.mNoOverlappingRules = False self.mTouchedObjectGroups = QSet() self.mWarning = QString() self.mTouchedTileLayers = QSet() self.mError = '' if (not self.setupRuleMapProperties()): return if (not self.setupRuleMapTileLayers()): return if (not self.setupRuleList()): return def __del__(self): self.cleanUpRulesMap() ## # Checks if the passed \a ruleLayerName is used in this instance # of Automapper. ## def ruleLayerNameUsed(self, ruleLayerName): return self.mInputRules.names.contains(ruleLayerName) ## # Call prepareLoad first! Returns a set of strings describing the tile # layers, which could be touched considering the given layers of the # rule map. ## def getTouchedTileLayers(self): return self.mTouchedTileLayers ## # This needs to be called directly before the autoMap call. # It sets up some data structures which change rapidly, so it is quite # painful to keep these datastructures up to date all time. (indices of # layers of the working map) ## def prepareAutoMap(self): self.mError = '' self.mWarning = '' if (not self.setupMissingLayers()): return False if (not self.setupCorrectIndexes()): return False if (not self.setupTilesets(self.mMapRules, self.mMapWork)): return False return True ## # Here is done all the automapping. ## def autoMap(self, where): # first resize the active area if (self.mAutoMappingRadius): region = QRegion() for r in where.rects(): region += r.adjusted(-self.mAutoMappingRadius, -self.mAutoMappingRadius, +self.mAutoMappingRadius, +self.mAutoMappingRadius) #where += region # delete all the relevant area, if the property "DeleteTiles" is set if (self.mDeleteTiles): setLayersRegion = self.getSetLayersRegion() for i in range(self.mLayerList.size()): translationTable = self.mLayerList.at(i) for layer in translationTable.keys(): index = self.mLayerList.at(i).value(layer) dstLayer = self.mMapWork.layerAt(index) region = setLayersRegion.intersected(where) dstTileLayer = dstLayer.asTileLayer() if (dstTileLayer): dstTileLayer.erase(region) else: self.eraseRegionObjectGroup(self.mMapDocument, dstLayer.asObjectGroup(), region) # Increase the given region where the next automapper should work. # This needs to be done, so you can rely on the order of the rules at all # locations ret = QRegion() for rect in where.rects(): for i in range(self.mRulesInput.size()): # at the moment the parallel execution does not work yet # TODO: make multithreading available! # either by dividing the rules or the region to multiple threads ret = ret.united(self.applyRule(i, rect)) #where = where.united(ret) ## # This cleans all datastructures, which are setup via prepareAutoMap, # so the auto mapper becomes ready for its next automatic mapping. ## def cleanAll(self): self.cleanTilesets() self.cleanTileLayers() ## # Contains all errors until operation was canceled. # The errorlist is cleared within prepareLoad and prepareAutoMap. ## def errorString(self): return self.mError ## # Contains all warnings which occur at loading a rules map or while # automapping. # The errorlist is cleared within prepareLoad and prepareAutoMap. ## def warningString(self): return self.mWarning ## # Reads the map properties of the rulesmap. # @return returns True when anything is ok, False when errors occured. ## def setupRuleMapProperties(self): properties = self.mMapRules.properties() for key in properties.keys(): value = properties.value(key) raiseWarning = True if (key.toLower() == "deletetiles"): if (value.canConvert(QVariant.Bool)): self.mDeleteTiles = value.toBool() raiseWarning = False elif (key.toLower() == "automappingradius"): if (value.canConvert(QVariant.Int)): self.mAutoMappingRadius = value raiseWarning = False elif (key.toLower() == "nooverlappingrules"): if (value.canConvert(QVariant.Bool)): self.mNoOverlappingRules = value.toBool() raiseWarning = False if (raiseWarning): self.mWarning += self.tr( "'%s': Property '%s' = '%s' does not make sense. \nIgnoring this property." % (self.mRulePath, key, value.toString()) + '\n') return True def cleanUpRulesMap(self): self.cleanTilesets() # mMapRules can be empty, when in prepareLoad the very first stages fail. if (not self.mMapRules): return tilesetManager = TilesetManager.instance() tilesetManager.removeReferences(self.mMapRules.tilesets()) del self.mMapRules self.mMapRules = None self.cleanUpRuleMapLayers() self.mRulesInput.clear() self.mRulesOutput.clear() ## # Searches the rules layer for regions and stores these in \a rules. # @return returns True when anything is ok, False when errors occured. ## def setupRuleList(self): combinedRegions = coherentRegions(self.mLayerInputRegions.region() + self.mLayerOutputRegions.region()) combinedRegions = QList( sorted(combinedRegions, key=lambda x: x.y(), reverse=True)) rulesInput = coherentRegions(self.mLayerInputRegions.region()) rulesOutput = coherentRegions(self.mLayerOutputRegions.region()) for i in range(combinedRegions.size()): self.mRulesInput.append(QRegion()) self.mRulesOutput.append(QRegion()) for reg in rulesInput: for i in range(combinedRegions.size()): if (reg.intersects(combinedRegions[i])): self.mRulesInput[i] += reg break for reg in rulesOutput: for i in range(combinedRegions.size()): if (reg.intersects(combinedRegions[i])): self.mRulesOutput[i] += reg break for i in range(self.mRulesInput.size()): checkCoherent = self.mRulesInput.at(i).united( self.mRulesOutput.at(i)) coherentRegions(checkCoherent).length() == 1 return True ## # Sets up the layers in the rules map, which are used for automapping. # The layers are detected and put in the internal data structures # @return returns True when anything is ok, False when errors occured. ## def setupRuleMapTileLayers(self): error = QString() for layer in self.mMapRules.layers(): layerName = layer.name() if (layerName.lower().startswith("regions")): treatAsBoth = layerName.toLower() == "regions" if (layerName.lower().endswith("input") or treatAsBoth): if (self.mLayerInputRegions): error += self.tr( "'regions_input' layer must not occur more than once.\n" ) if (layer.isTileLayer()): self.mLayerInputRegions = layer.asTileLayer() else: error += self.tr( "'regions_*' layers must be tile layers.\n") if (layerName.lower().endswith("output") or treatAsBoth): if (self.mLayerOutputRegions): error += self.tr( "'regions_output' layer must not occur more than once.\n" ) if (layer.isTileLayer()): self.mLayerOutputRegions = layer.asTileLayer() else: error += self.tr( "'regions_*' layers must be tile layers.\n") continue nameStartPosition = layerName.indexOf('_') + 1 # name is all characters behind the underscore (excluded) name = layerName.right(layerName.size() - nameStartPosition) # group is all before the underscore (included) index = layerName.left(nameStartPosition) if (index.lower().startswith("output")): index.remove(0, 6) elif (index.lower().startswith("inputnot")): index.remove(0, 8) elif (index.lower().startswith("input")): index.remove(0, 5) # both 'rule' and 'output' layers will require and underscore and # rely on the correct position detected of the underscore if (nameStartPosition == 0): error += self.tr( "Did you forget an underscore in layer '%d'?\n" % layerName) continue if (layerName.startsWith("input", Qt.CaseInsensitive)): isNotList = layerName.lower().startswith("inputnot") if (not layer.isTileLayer()): error += self.tr( "'input_*' and 'inputnot_*' layers must be tile layers.\n" ) continue self.mInputRules.names.insert(name) if (not self.mInputRules.indexes.contains(index)): self.mInputRules.indexes.insert(index) self.mInputRules.insert(index, InputIndex()) if (not self.mInputRules[index].names.contains(name)): self.mInputRules[index].names.insert(name) self.mInputRules[index].insert(name, InputIndexName()) if (isNotList): self.mInputRules[index][name].listNo.append( layer.asTileLayer()) else: self.mInputRules[index][name].listYes.append( layer.asTileLayer()) continue if layerName.lower().startswith("output"): if (layer.isTileLayer()): self.mTouchedTileLayers.insert(name) else: self.mTouchedObjectGroups.insert(name) type = layer.layerType() layerIndex = self.mMapWork.indexOfLayer(name, type) found = False for translationTable in self.mLayerList: if (translationTable.index == index): translationTable.insert(layer, layerIndex) found = True break if (not found): self.mLayerList.append(RuleOutput()) self.mLayerList.last().insert(layer, layerIndex) self.mLayerList.last().index = index continue error += self.tr( "Layer '%s' is not recognized as a valid layer for Automapping.\n" % layerName) if (not self.mLayerInputRegions): error += self.tr("No 'regions' or 'regions_input' layer found.\n") if (not self.mLayerOutputRegions): error += self.tr("No 'regions' or 'regions_output' layer found.\n") if (self.mInputRules.isEmpty()): error += self.tr("No input_<name> layer found!\n") # no need to check for mInputNotRules.size() == 0 here. # these layers are not necessary. if error != '': error = self.mRulePath + '\n' + error self.mError += error return False return True ## # Checks if all needed layers in the working map are there. # If not, add them in the correct order. ## def setupMissingLayers(self): # make sure all needed layers are there: for name in self.mTouchedTileLayers: if (self.mMapWork.indexOfLayer(name, Layer.TileLayerType) != -1): continue index = self.mMapWork.layerCount() tilelayer = TileLayer(name, 0, 0, self.mMapWork.width(), self.mMapWork.height()) self.mMapDocument.undoStack().push( AddLayer(self.mMapDocument, index, tilelayer)) self.mAddedTileLayers.append(name) for name in self.mTouchedObjectGroups: if (self.mMapWork.indexOfLayer(name, Layer.ObjectGroupType) != -1): continue index = self.mMapWork.layerCount() objectGroup = ObjectGroup(name, 0, 0, self.mMapWork.width(), self.mMapWork.height()) self.mMapDocument.undoStack().push( AddLayer(self.mMapDocument, index, objectGroup)) self.mAddedTileLayers.append(name) return True ## # Checks if the layers setup as in setupRuleMapLayers are still right. # If it's not right, correct them. # @return returns True if everything went fine. False is returned when # no set layer was found ## def setupCorrectIndexes(self): # make sure all indexes of the layer translationtables are correct. for i in range(self.mLayerList.size()): translationTable = self.mLayerList.at(i) for layerKey in translationTable.keys(): name = layerKey.name() pos = name.indexOf('_') + 1 name = name.right(name.length() - pos) index = translationTable.value(layerKey, -1) if (index >= self.mMapWork.layerCount() or index == -1 or name != self.mMapWork.layerAt(index).name()): newIndex = self.mMapWork.indexOfLayer( name, layerKey.layerType()) translationTable.insert(layerKey, newIndex) return True ## # sets up the tilesets which are used in automapping. # @return returns True when anything is ok, False when errors occured. # (in that case will be a msg box anyway) ## # This cannot just be replaced by MapDocument::unifyTileset(Map), # because here mAddedTileset is modified. def setupTilesets(self, src, dst): existingTilesets = dst.tilesets() tilesetManager = TilesetManager.instance() # Add tilesets that are not yet part of dst map for tileset in src.tilesets(): if (existingTilesets.contains(tileset)): continue undoStack = self.mMapDocument.undoStack() replacement = tileset.findSimilarTileset(existingTilesets) if (not replacement): self.mAddedTilesets.append(tileset) undoStack.push(AddTileset(self.mMapDocument, tileset)) continue # Merge the tile properties sharedTileCount = min(tileset.tileCount(), replacement.tileCount()) for i in range(sharedTileCount): replacementTile = replacement.tileAt(i) properties = replacementTile.properties() properties.merge(tileset.tileAt(i).properties()) undoStack.push( ChangeProperties(self.mMapDocument, self.tr("Tile"), replacementTile, properties)) src.replaceTileset(tileset, replacement) tilesetManager.addReference(replacement) tilesetManager.removeReference(tileset) return True ## # Returns the conjunction of of all regions of all setlayers ## def getSetLayersRegion(self): result = QRegion() for name in self.mInputRules.names: index = self.mMapWork.indexOfLayer(name, Layer.TileLayerType) if (index == -1): continue setLayer = self.mMapWork.layerAt(index).asTileLayer() result |= setLayer.region() return result ## # This copies all Tiles from TileLayer src to TileLayer dst # # In src the Tiles are taken from the rectangle given by # src_x, src_y, width and height. # In dst they get copied to a rectangle given by # dst_x, dst_y, width, height . # if there is no tile in src TileLayer, there will nothing be copied, # so the maybe existing tile in dst will not be overwritten. # ## def copyTileRegion(self, srcLayer, srcX, srcY, width, height, dstLayer, dstX, dstY): startX = max(dstX, 0) startY = max(dstY, 0) endX = min(dstX + width, dstLayer.width()) endY = min(dstY + height, dstLayer.height()) offsetX = srcX - dstX offsetY = srcY - dstY for x in range(startX, endX): for y in range(startY, endY): cell = srcLayer.cellAt(x + offsetX, y + offsetY) if (not cell.isEmpty()): # this is without graphics update, it's done afterwards for all dstLayer.setCell(x, y, cell) ## # This copies all objects from the \a src_lr ObjectGroup to the \a dst_lr # in the given rectangle. # # The rectangle is described by the upper left corner \a src_x \a src_y # and its \a width and \a height. The parameter \a dst_x and \a dst_y # offset the copied objects in the destination object group. ## def copyObjectRegion(self, srcLayer, srcX, srcY, width, height, dstLayer, dstX, dstY): undo = self.mMapDocument.undoStack() rect = QRectF(srcX, srcY, width, height) pixelRect = self.mMapDocument.renderer().tileToPixelCoords_(rect) objects = objectsInRegion(srcLayer, pixelRect.toAlignedRect()) pixelOffset = self.mMapDocument.renderer().tileToPixelCoords( dstX, dstY) pixelOffset -= pixelRect.topLeft() clones = QList() for obj in objects: clone = obj.clone() clones.append(clone) clone.setX(clone.x() + pixelOffset.x()) clone.setY(clone.y() + pixelOffset.y()) undo.push(AddMapObject(self.mMapDocument, dstLayer, clone)) ## # This copies multiple TileLayers from one map to another. # Only the region \a region is considered for copying. # In the destination it will come to the region translated by Offset. # The parameter \a LayerTranslation is a map of which layers of the rulesmap # should get copied into which layers of the working map. ## def copyMapRegion(self, region, offset, layerTranslation): for i in range(layerTranslation.keys().size()): _from = layerTranslation.keys().at(i) to = self.mMapWork.layerAt(layerTranslation.value(_from)) for rect in region.rects(): fromTileLayer = _from.asTileLayer() fromObjectGroup = _from.asObjectGroup() if (fromTileLayer): toTileLayer = to.asTileLayer() self.copyTileRegion(fromTileLayer, rect.x(), rect.y(), rect.width(), rect.height(), toTileLayer, rect.x() + offset.x(), rect.y() + offset.y()) elif (fromObjectGroup): toObjectGroup = to.asObjectGroup() self.copyObjectRegion(fromObjectGroup, rect.x(), rect.y(), rect.width(), rect.height(), toObjectGroup, rect.x() + offset.x(), rect.y() + offset.y()) else: pass ## # This goes through all the positions of the mMapWork and checks if # there fits the rule given by the region in mMapRuleSet. # if there is a match all Layers are copied to mMapWork. # @param ruleIndex: the region which should be compared to all positions # of mMapWork will be looked up in mRulesInput and mRulesOutput # @return where: an rectangle where the rule actually got applied ## def applyRule(self, ruleIndex, where): ret = QRect() if (self.mLayerList.isEmpty()): return ret ruleInput = self.mRulesInput.at(ruleIndex) ruleOutput = self.mRulesOutput.at(ruleIndex) rbr = ruleInput.boundingRect() # Since the rule itself is translated, we need to adjust the borders of the # loops. Decrease the size at all sides by one: There must be at least one # tile overlap to the rule. minX = where.left() - rbr.left() - rbr.width() + 1 minY = where.top() - rbr.top() - rbr.height() + 1 maxX = where.right() - rbr.left() + rbr.width() - 1 maxY = where.bottom() - rbr.top() + rbr.height() - 1 # In this list of regions it is stored which parts or the map have already # been altered by exactly this rule. We store all the altered parts to # make sure there are no overlaps of the same rule applied to # (neighbouring) places appliedRegions = QList() if (self.mNoOverlappingRules): for i in range(self.mMapWork.layerCount()): appliedRegions.append(QRegion()) for y in range(minY, maxY + 1): for x in range(minX, maxX + 1): anymatch = False for index in self.mInputRules.indexes: ii = self.mInputRules[index] allLayerNamesMatch = True for name in ii.names: i = self.mMapWork.indexOfLayer(name, Layer.TileLayerType) if (i == -1): allLayerNamesMatch = False else: setLayer = self.mMapWork.layerAt(i).asTileLayer() allLayerNamesMatch &= compareLayerTo( setLayer, ii[name].listYes, ii[name].listNo, ruleInput, QPoint(x, y)) if (allLayerNamesMatch): anymatch = True break if (anymatch): r = 0 # choose by chance which group of rule_layers should be used: if (self.mLayerList.size() > 1): r = qrand() % self.mLayerList.size() if (not self.mNoOverlappingRules): self.copyMapRegion(ruleOutput, QPoint(x, y), self.mLayerList.at(r)) ret = ret.united(rbr.translated(QPoint(x, y))) continue missmatch = False translationTable = self.mLayerList.at(r) layers = translationTable.keys() # check if there are no overlaps within this rule. ruleRegionInLayer = QVector() for i in range(layers.size()): layer = layers.at(i) appliedPlace = QRegion() tileLayer = layer.asTileLayer() if (tileLayer): appliedPlace = tileLayer.region() else: appliedPlace = tileRegionOfObjectGroup( layer.asObjectGroup()) ruleRegionInLayer.append( appliedPlace.intersected(ruleOutput)) if (appliedRegions.at(i).intersects( ruleRegionInLayer[i].translated(x, y))): missmatch = True break if (missmatch): continue self.copyMapRegion(ruleOutput, QPoint(x, y), self.mLayerList.at(r)) ret = ret.united(rbr.translated(QPoint(x, y))) for i in range(translationTable.size()): appliedRegions[i] += ruleRegionInLayer[i].translated( x, y) return ret ## # Cleans up the data structes filled by setupRuleMapLayers(), # so the next rule can be processed. ## def cleanUpRuleMapLayers(self): self.cleanTileLayers() it = QList.const_iterator() for it in self.mLayerList: del it self.mLayerList.clear() # do not delete mLayerRuleRegions, it is owned by the rulesmap self.mLayerInputRegions = None self.mLayerOutputRegions = None self.mInputRules.clear() ## # Cleans up the data structes filled by setupTilesets(), # so the next rule can be processed. ## def cleanTilesets(self): for tileset in self.mAddedTilesets: if (self.mMapWork.isTilesetUsed(tileset)): continue index = self.mMapWork.indexOfTileset(tileset) if (index == -1): continue undo = self.mMapDocument.undoStack() undo.push(RemoveTileset(self.mMapDocument, index)) self.mAddedTilesets.clear() ## # Cleans up the added tile layers setup by setupMissingLayers(), # so we have a minimal addition of tile layers by the automapping. ## def cleanTileLayers(self): for tilelayerName in self.mAddedTileLayers: layerIndex = self.mMapWork.indexOfLayer(tilelayerName, Layer.TileLayerType) if (layerIndex == -1): continue layer = self.mMapWork.layerAt(layerIndex) if (not layer.isEmpty()): continue undo = self.mMapDocument.undoStack() undo.push(RemoveLayer(self.mMapDocument, layerIndex)) self.mAddedTileLayers.clear()
class AutoMapper(QObject): ## # Constructs an AutoMapper. # All data structures, which only rely on the rules map are setup # here. # # @param workingDocument: the map to work on. # @param rules: The rule map which should be used for automapping # @param rulePath: The filepath to the rule map. ## def __init__(self, workingDocument, rules, rulePath): ## # where to work in ## self.mMapDocument = workingDocument ## # the same as mMapDocument.map() ## self.mMapWork = None if workingDocument: self.mMapWork = workingDocument.map() ## # map containing the rules, usually different than mMapWork ## self.mMapRules = rules ## # This contains all added tilesets as pointers. # if rules use Tilesets which are not in the mMapWork they are added. # keep track of them, because we need to delete them afterwards, # when they still are unused # they will be added while setupTilesets(). ## self.mAddedTilesets = QVector() ## # description see: mAddedTilesets, just described by Strings ## self.mAddedTileLayers = QList() ## # Points to the tilelayer, which defines the inputregions. ## self.mLayerInputRegions = None ## # Points to the tilelayer, which defines the outputregions. ## self.mLayerOutputRegions = None ## # Contains all tilelayer pointers, which names begin with input* # It is sorted by index and name ## self.mInputRules = InputLayers() ## # List of Regions in mMapRules to know where the input rules are ## self.mRulesInput = QList() ## # List of regions in mMapRules to know where the output of a # rule is. # mRulesOutput[i] is the output of that rule, # which has the input at mRulesInput[i], meaning that mRulesInput # and mRulesOutput must match with the indexes. ## self.mRulesOutput = QList() ## # The inner set with layers to indexes is needed for translating # tile layers from mMapRules to mMapWork. # # The key is the pointer to the layer in the rulemap. The # pointer to the layer within the working map is not hardwired, but the # position in the layerlist, where it was found the last time. # This loosely bound pointer ensures we will get the right layer, since we # need to check before anyway, and it is still fast. # # The list is used to hold different translation tables # => one of the tables is chosen by chance, so randomness is available ## self.mLayerList = QList() ## # store the name of the processed rules file, to have detailed # error messages available ## self.mRulePath = rulePath ## # determines if all tiles in all touched layers should be deleted first. ## self.mDeleteTiles = False ## # This variable determines, how many overlapping tiles should be used. # The bigger the more area is remapped at an automapping operation. # This can lead to higher latency, but provides a better behavior on # interactive automapping. # It defaults to zero. ## self.mAutoMappingRadius = 0 ## # Determines if a rule is allowed to overlap it ## self.mNoOverlappingRules = False self.mTouchedObjectGroups = QSet() self.mWarning = QString() self.mTouchedTileLayers = QSet() self.mError = '' if (not self.setupRuleMapProperties()): return if (not self.setupRuleMapTileLayers()): return if (not self.setupRuleList()): return def __del__(self): self.cleanUpRulesMap() ## # Checks if the passed \a ruleLayerName is used in this instance # of Automapper. ## def ruleLayerNameUsed(self, ruleLayerName): return self.mInputRules.names.contains(ruleLayerName) ## # Call prepareLoad first! Returns a set of strings describing the tile # layers, which could be touched considering the given layers of the # rule map. ## def getTouchedTileLayers(self): return self.mTouchedTileLayers ## # This needs to be called directly before the autoMap call. # It sets up some data structures which change rapidly, so it is quite # painful to keep these datastructures up to date all time. (indices of # layers of the working map) ## def prepareAutoMap(self): self.mError = '' self.mWarning = '' if (not self.setupMissingLayers()): return False if (not self.setupCorrectIndexes()): return False if (not self.setupTilesets(self.mMapRules, self.mMapWork)): return False return True ## # Here is done all the automapping. ## def autoMap(self, where): # first resize the active area if (self.mAutoMappingRadius): region = QRegion() for r in where.rects(): region += r.adjusted(- self.mAutoMappingRadius, - self.mAutoMappingRadius, + self.mAutoMappingRadius, + self.mAutoMappingRadius) #where += region # delete all the relevant area, if the property "DeleteTiles" is set if (self.mDeleteTiles): setLayersRegion = self.getSetLayersRegion() for i in range(self.mLayerList.size()): translationTable = self.mLayerList.at(i) for layer in translationTable.keys(): index = self.mLayerList.at(i).value(layer) dstLayer = self.mMapWork.layerAt(index) region = setLayersRegion.intersected(where) dstTileLayer = dstLayer.asTileLayer() if (dstTileLayer): dstTileLayer.erase(region) else: self.eraseRegionObjectGroup(self.mMapDocument, dstLayer.asObjectGroup(), region) # Increase the given region where the next automapper should work. # This needs to be done, so you can rely on the order of the rules at all # locations ret = QRegion() for rect in where.rects(): for i in range(self.mRulesInput.size()): # at the moment the parallel execution does not work yet # TODO: make multithreading available! # either by dividing the rules or the region to multiple threads ret = ret.united(self.applyRule(i, rect)) #where = where.united(ret) ## # This cleans all datastructures, which are setup via prepareAutoMap, # so the auto mapper becomes ready for its next automatic mapping. ## def cleanAll(self): self.cleanTilesets() self.cleanTileLayers() ## # Contains all errors until operation was canceled. # The errorlist is cleared within prepareLoad and prepareAutoMap. ## def errorString(self): return self.mError ## # Contains all warnings which occur at loading a rules map or while # automapping. # The errorlist is cleared within prepareLoad and prepareAutoMap. ## def warningString(self): return self.mWarning ## # Reads the map properties of the rulesmap. # @return returns True when anything is ok, False when errors occured. ## def setupRuleMapProperties(self): properties = self.mMapRules.properties() for key in properties.keys(): value = properties.value(key) raiseWarning = True if (key.toLower() == "deletetiles"): if (value.canConvert(QVariant.Bool)): self.mDeleteTiles = value.toBool() raiseWarning = False elif (key.toLower() == "automappingradius"): if (value.canConvert(QVariant.Int)): self.mAutoMappingRadius = value raiseWarning = False elif (key.toLower() == "nooverlappingrules"): if (value.canConvert(QVariant.Bool)): self.mNoOverlappingRules = value.toBool() raiseWarning = False if (raiseWarning): self.mWarning += self.tr("'%s': Property '%s' = '%s' does not make sense. \nIgnoring this property."%(self.mRulePath, key, value.toString()) + '\n') return True def cleanUpRulesMap(self): self.cleanTilesets() # mMapRules can be empty, when in prepareLoad the very first stages fail. if (not self.mMapRules): return tilesetManager = TilesetManager.instance() tilesetManager.removeReferences(self.mMapRules.tilesets()) del self.mMapRules self.mMapRules = None self.cleanUpRuleMapLayers() self.mRulesInput.clear() self.mRulesOutput.clear() ## # Searches the rules layer for regions and stores these in \a rules. # @return returns True when anything is ok, False when errors occured. ## def setupRuleList(self): combinedRegions = coherentRegions( self.mLayerInputRegions.region() + self.mLayerOutputRegions.region()) combinedRegions = QList(sorted(combinedRegions, key=lambda x:x.y(), reverse=True)) rulesInput = coherentRegions( self.mLayerInputRegions.region()) rulesOutput = coherentRegions( self.mLayerOutputRegions.region()) for i in range(combinedRegions.size()): self.mRulesInput.append(QRegion()) self.mRulesOutput.append(QRegion()) for reg in rulesInput: for i in range(combinedRegions.size()): if (reg.intersects(combinedRegions[i])): self.mRulesInput[i] += reg break for reg in rulesOutput: for i in range(combinedRegions.size()): if (reg.intersects(combinedRegions[i])): self.mRulesOutput[i] += reg break for i in range(self.mRulesInput.size()): checkCoherent = self.mRulesInput.at(i).united(self.mRulesOutput.at(i)) coherentRegions(checkCoherent).length() == 1 return True ## # Sets up the layers in the rules map, which are used for automapping. # The layers are detected and put in the internal data structures # @return returns True when anything is ok, False when errors occured. ## def setupRuleMapTileLayers(self): error = QString() for layer in self.mMapRules.layers(): layerName = layer.name() if (layerName.lower().startswith("regions")): treatAsBoth = layerName.toLower() == "regions" if (layerName.lower().endswith("input") or treatAsBoth): if (self.mLayerInputRegions): error += self.tr("'regions_input' layer must not occur more than once.\n") if (layer.isTileLayer()): self.mLayerInputRegions = layer.asTileLayer() else: error += self.tr("'regions_*' layers must be tile layers.\n") if (layerName.lower().endswith("output") or treatAsBoth): if (self.mLayerOutputRegions): error += self.tr("'regions_output' layer must not occur more than once.\n") if (layer.isTileLayer()): self.mLayerOutputRegions = layer.asTileLayer() else: error += self.tr("'regions_*' layers must be tile layers.\n") continue nameStartPosition = layerName.indexOf('_') + 1 # name is all characters behind the underscore (excluded) name = layerName.right(layerName.size() - nameStartPosition) # group is all before the underscore (included) index = layerName.left(nameStartPosition) if (index.lower().startswith("output")): index.remove(0, 6) elif (index.lower().startswith("inputnot")): index.remove(0, 8) elif (index.lower().startswith("input")): index.remove(0, 5) # both 'rule' and 'output' layers will require and underscore and # rely on the correct position detected of the underscore if (nameStartPosition == 0): error += self.tr("Did you forget an underscore in layer '%d'?\n"%layerName) continue if (layerName.startsWith("input", Qt.CaseInsensitive)): isNotList = layerName.lower().startswith("inputnot") if (not layer.isTileLayer()): error += self.tr("'input_*' and 'inputnot_*' layers must be tile layers.\n") continue self.mInputRules.names.insert(name) if (not self.mInputRules.indexes.contains(index)): self.mInputRules.indexes.insert(index) self.mInputRules.insert(index, InputIndex()) if (not self.mInputRules[index].names.contains(name)): self.mInputRules[index].names.insert(name) self.mInputRules[index].insert(name, InputIndexName()) if (isNotList): self.mInputRules[index][name].listNo.append(layer.asTileLayer()) else: self.mInputRules[index][name].listYes.append(layer.asTileLayer()) continue if layerName.lower().startswith("output"): if (layer.isTileLayer()): self.mTouchedTileLayers.insert(name) else: self.mTouchedObjectGroups.insert(name) type = layer.layerType() layerIndex = self.mMapWork.indexOfLayer(name, type) found = False for translationTable in self.mLayerList: if (translationTable.index == index): translationTable.insert(layer, layerIndex) found = True break if (not found): self.mLayerList.append(RuleOutput()) self.mLayerList.last().insert(layer, layerIndex) self.mLayerList.last().index = index continue error += self.tr("Layer '%s' is not recognized as a valid layer for Automapping.\n"%layerName) if (not self.mLayerInputRegions): error += self.tr("No 'regions' or 'regions_input' layer found.\n") if (not self.mLayerOutputRegions): error += self.tr("No 'regions' or 'regions_output' layer found.\n") if (self.mInputRules.isEmpty()): error += self.tr("No input_<name> layer found!\n") # no need to check for mInputNotRules.size() == 0 here. # these layers are not necessary. if error != '': error = self.mRulePath + '\n' + error self.mError += error return False return True ## # Checks if all needed layers in the working map are there. # If not, add them in the correct order. ## def setupMissingLayers(self): # make sure all needed layers are there: for name in self.mTouchedTileLayers: if (self.mMapWork.indexOfLayer(name, Layer.TileLayerType) != -1): continue index = self.mMapWork.layerCount() tilelayer = TileLayer(name, 0, 0, self.mMapWork.width(), self.mMapWork.height()) self.mMapDocument.undoStack().push(AddLayer(self.mMapDocument, index, tilelayer)) self.mAddedTileLayers.append(name) for name in self.mTouchedObjectGroups: if (self.mMapWork.indexOfLayer(name, Layer.ObjectGroupType) != -1): continue index = self.mMapWork.layerCount() objectGroup = ObjectGroup(name, 0, 0, self.mMapWork.width(), self.mMapWork.height()) self.mMapDocument.undoStack().push(AddLayer(self.mMapDocument, index, objectGroup)) self.mAddedTileLayers.append(name) return True ## # Checks if the layers setup as in setupRuleMapLayers are still right. # If it's not right, correct them. # @return returns True if everything went fine. False is returned when # no set layer was found ## def setupCorrectIndexes(self): # make sure all indexes of the layer translationtables are correct. for i in range(self.mLayerList.size()): translationTable = self.mLayerList.at(i) for layerKey in translationTable.keys(): name = layerKey.name() pos = name.indexOf('_') + 1 name = name.right(name.length() - pos) index = translationTable.value(layerKey, -1) if (index >= self.mMapWork.layerCount() or index == -1 or name != self.mMapWork.layerAt(index).name()): newIndex = self.mMapWork.indexOfLayer(name, layerKey.layerType()) translationTable.insert(layerKey, newIndex) return True ## # sets up the tilesets which are used in automapping. # @return returns True when anything is ok, False when errors occured. # (in that case will be a msg box anyway) ## # This cannot just be replaced by MapDocument::unifyTileset(Map), # because here mAddedTileset is modified. def setupTilesets(self, src, dst): existingTilesets = dst.tilesets() tilesetManager = TilesetManager.instance() # Add tilesets that are not yet part of dst map for tileset in src.tilesets(): if (existingTilesets.contains(tileset)): continue undoStack = self.mMapDocument.undoStack() replacement = tileset.findSimilarTileset(existingTilesets) if (not replacement): self.mAddedTilesets.append(tileset) undoStack.push(AddTileset(self.mMapDocument, tileset)) continue # Merge the tile properties sharedTileCount = min(tileset.tileCount(), replacement.tileCount()) for i in range(sharedTileCount): replacementTile = replacement.tileAt(i) properties = replacementTile.properties() properties.merge(tileset.tileAt(i).properties()) undoStack.push(ChangeProperties(self.mMapDocument, self.tr("Tile"), replacementTile, properties)) src.replaceTileset(tileset, replacement) tilesetManager.addReference(replacement) tilesetManager.removeReference(tileset) return True ## # Returns the conjunction of of all regions of all setlayers ## def getSetLayersRegion(self): result = QRegion() for name in self.mInputRules.names: index = self.mMapWork.indexOfLayer(name, Layer.TileLayerType) if (index == -1): continue setLayer = self.mMapWork.layerAt(index).asTileLayer() result |= setLayer.region() return result ## # This copies all Tiles from TileLayer src to TileLayer dst # # In src the Tiles are taken from the rectangle given by # src_x, src_y, width and height. # In dst they get copied to a rectangle given by # dst_x, dst_y, width, height . # if there is no tile in src TileLayer, there will nothing be copied, # so the maybe existing tile in dst will not be overwritten. # ## def copyTileRegion(self, srcLayer, srcX, srcY, width, height, dstLayer, dstX, dstY): startX = max(dstX, 0) startY = max(dstY, 0) endX = min(dstX + width, dstLayer.width()) endY = min(dstY + height, dstLayer.height()) offsetX = srcX - dstX offsetY = srcY - dstY for x in range(startX, endX): for y in range(startY, endY): cell = srcLayer.cellAt(x + offsetX, y + offsetY) if (not cell.isEmpty()): # this is without graphics update, it's done afterwards for all dstLayer.setCell(x, y, cell) ## # This copies all objects from the \a src_lr ObjectGroup to the \a dst_lr # in the given rectangle. # # The rectangle is described by the upper left corner \a src_x \a src_y # and its \a width and \a height. The parameter \a dst_x and \a dst_y # offset the copied objects in the destination object group. ## def copyObjectRegion(self, srcLayer, srcX, srcY, width, height, dstLayer, dstX, dstY): undo = self.mMapDocument.undoStack() rect = QRectF(srcX, srcY, width, height) pixelRect = self.mMapDocument.renderer().tileToPixelCoords_(rect) objects = objectsInRegion(srcLayer, pixelRect.toAlignedRect()) pixelOffset = self.mMapDocument.renderer().tileToPixelCoords(dstX, dstY) pixelOffset -= pixelRect.topLeft() clones = QList() for obj in objects: clone = obj.clone() clones.append(clone) clone.setX(clone.x() + pixelOffset.x()) clone.setY(clone.y() + pixelOffset.y()) undo.push(AddMapObject(self.mMapDocument, dstLayer, clone)) ## # This copies multiple TileLayers from one map to another. # Only the region \a region is considered for copying. # In the destination it will come to the region translated by Offset. # The parameter \a LayerTranslation is a map of which layers of the rulesmap # should get copied into which layers of the working map. ## def copyMapRegion(self, region, offset, layerTranslation): for i in range(layerTranslation.keys().size()): _from = layerTranslation.keys().at(i) to = self.mMapWork.layerAt(layerTranslation.value(_from)) for rect in region.rects(): fromTileLayer = _from.asTileLayer() fromObjectGroup = _from.asObjectGroup() if (fromTileLayer): toTileLayer = to.asTileLayer() self.copyTileRegion(fromTileLayer, rect.x(), rect.y(), rect.width(), rect.height(), toTileLayer, rect.x() + offset.x(), rect.y() + offset.y()) elif (fromObjectGroup): toObjectGroup = to.asObjectGroup() self.copyObjectRegion(fromObjectGroup, rect.x(), rect.y(), rect.width(), rect.height(), toObjectGroup, rect.x() + offset.x(), rect.y() + offset.y()) else: pass ## # This goes through all the positions of the mMapWork and checks if # there fits the rule given by the region in mMapRuleSet. # if there is a match all Layers are copied to mMapWork. # @param ruleIndex: the region which should be compared to all positions # of mMapWork will be looked up in mRulesInput and mRulesOutput # @return where: an rectangle where the rule actually got applied ## def applyRule(self, ruleIndex, where): ret = QRect() if (self.mLayerList.isEmpty()): return ret ruleInput = self.mRulesInput.at(ruleIndex) ruleOutput = self.mRulesOutput.at(ruleIndex) rbr = ruleInput.boundingRect() # Since the rule itself is translated, we need to adjust the borders of the # loops. Decrease the size at all sides by one: There must be at least one # tile overlap to the rule. minX = where.left() - rbr.left() - rbr.width() + 1 minY = where.top() - rbr.top() - rbr.height() + 1 maxX = where.right() - rbr.left() + rbr.width() - 1 maxY = where.bottom() - rbr.top() + rbr.height() - 1 # In this list of regions it is stored which parts or the map have already # been altered by exactly this rule. We store all the altered parts to # make sure there are no overlaps of the same rule applied to # (neighbouring) places appliedRegions = QList() if (self.mNoOverlappingRules): for i in range(self.mMapWork.layerCount()): appliedRegions.append(QRegion()) for y in range(minY, maxY+1): for x in range(minX, maxX+1): anymatch = False for index in self.mInputRules.indexes: ii = self.mInputRules[index] allLayerNamesMatch = True for name in ii.names: i = self.mMapWork.indexOfLayer(name, Layer.TileLayerType) if (i == -1): allLayerNamesMatch = False else: setLayer = self.mMapWork.layerAt(i).asTileLayer() allLayerNamesMatch &= compareLayerTo(setLayer, ii[name].listYes, ii[name].listNo, ruleInput, QPoint(x, y)) if (allLayerNamesMatch): anymatch = True break if (anymatch): r = 0 # choose by chance which group of rule_layers should be used: if (self.mLayerList.size() > 1): r = qrand() % self.mLayerList.size() if (not self.mNoOverlappingRules): self.copyMapRegion(ruleOutput, QPoint(x, y), self.mLayerList.at(r)) ret = ret.united(rbr.translated(QPoint(x, y))) continue missmatch = False translationTable = self.mLayerList.at(r) layers = translationTable.keys() # check if there are no overlaps within this rule. ruleRegionInLayer = QVector() for i in range(layers.size()): layer = layers.at(i) appliedPlace = QRegion() tileLayer = layer.asTileLayer() if (tileLayer): appliedPlace = tileLayer.region() else: appliedPlace = tileRegionOfObjectGroup(layer.asObjectGroup()) ruleRegionInLayer.append(appliedPlace.intersected(ruleOutput)) if (appliedRegions.at(i).intersects( ruleRegionInLayer[i].translated(x, y))): missmatch = True break if (missmatch): continue self.copyMapRegion(ruleOutput, QPoint(x, y), self.mLayerList.at(r)) ret = ret.united(rbr.translated(QPoint(x, y))) for i in range(translationTable.size()): appliedRegions[i] += ruleRegionInLayer[i].translated(x, y) return ret ## # Cleans up the data structes filled by setupRuleMapLayers(), # so the next rule can be processed. ## def cleanUpRuleMapLayers(self): self.cleanTileLayers() it = QList.const_iterator() for it in self.mLayerList: del it self.mLayerList.clear() # do not delete mLayerRuleRegions, it is owned by the rulesmap self.mLayerInputRegions = None self.mLayerOutputRegions = None self.mInputRules.clear() ## # Cleans up the data structes filled by setupTilesets(), # so the next rule can be processed. ## def cleanTilesets(self): for tileset in self.mAddedTilesets: if (self.mMapWork.isTilesetUsed(tileset)): continue index = self.mMapWork.indexOfTileset(tileset) if (index == -1): continue undo = self.mMapDocument.undoStack() undo.push(RemoveTileset(self.mMapDocument, index)) self.mAddedTilesets.clear() ## # Cleans up the added tile layers setup by setupMissingLayers(), # so we have a minimal addition of tile layers by the automapping. ## def cleanTileLayers(self): for tilelayerName in self.mAddedTileLayers: layerIndex = self.mMapWork.indexOfLayer(tilelayerName, Layer.TileLayerType) if (layerIndex == -1): continue layer = self.mMapWork.layerAt(layerIndex) if (not layer.isEmpty()): continue undo = self.mMapDocument.undoStack() undo.push(RemoveLayer(self.mMapDocument, layerIndex)) self.mAddedTileLayers.clear()
def usedTilesets(self): return QSet()