def autoMapInternal(self, where, touchedLayer): self.mError = '' self.mWarning = '' if (not self.mMapDocument): return automatic = touchedLayer != None if (not self.mLoaded): mapPath = QFileInfo(self.mMapDocument.fileName()).path() rulesFileName = mapPath + "/rules.txt" if (self.loadFile(rulesFileName)): self.mLoaded = True else: self.errorsOccurred.emit(automatic) return passedAutoMappers = QVector() if (touchedLayer): for a in self.mAutoMappers: if (a.ruleLayerNameUsed(touchedLayer.name())): passedAutoMappers.append(a) else: passedAutoMappers = self.mAutoMappers if (not passedAutoMappers.isEmpty()): # use a pointer to the region, so each automapper can manipulate it and the # following automappers do see the impact region = QRegion(where) undoStack = self.mMapDocument.undoStack() undoStack.beginMacro(self.tr("Apply AutoMap rules")) aw = AutoMapperWrapper(self.mMapDocument, passedAutoMappers, region) undoStack.push(aw) undoStack.endMacro() for automapper in self.mAutoMappers: self.mWarning += automapper.warningString() self.mError += automapper.errorString() if self.mWarning != '': self.warningsOccurred.emit(automatic) if self.mError != '': self.errorsOccurred.emit(automatic)
def autoMapInternal(self, where, touchedLayer): self.mError = '' self.mWarning = '' if (not self.mMapDocument): return automatic = touchedLayer != None if (not self.mLoaded): mapPath = QFileInfo(self.mMapDocument.fileName()).path() rulesFileName = mapPath + "/rules.txt" if (self.loadFile(rulesFileName)): self.mLoaded = True else: self.errorsOccurred.emit(automatic) return passedAutoMappers = QVector() if (touchedLayer): for a in self.mAutoMappers: if (a.ruleLayerNameUsed(touchedLayer.name())): passedAutoMappers.append(a) else: passedAutoMappers = self.mAutoMappers if (not passedAutoMappers.isEmpty()): # use a pointer to the region, so each automapper can manipulate it and the # following automappers do see the impact region = QRegion(where) undoStack = self.mMapDocument.undoStack() undoStack.beginMacro(self.tr("Apply AutoMap rules")) aw = AutoMapperWrapper(self.mMapDocument, passedAutoMappers, region) undoStack.push(aw) undoStack.endMacro() for automapper in self.mAutoMappers: self.mWarning += automapper.warningString() self.mError += automapper.errorString() if self.mWarning != '': self.warningsOccurred.emit(automatic) if self.mError != '': self.errorsOccurred.emit(automatic)
def dropMimeData(self, data, action, row, column, parent): if (action == Qt.IgnoreAction): return True if (column > 0): return False beginRow = 0 if (row != -1): beginRow = row elif parent.isValid(): beginRow = parent.row() else: beginRow = self.mFrames.size() newFrames = QVector() if (data.hasFormat(FRAMES_MIMETYPE)): encodedData = data.data(FRAMES_MIMETYPE) stream = QDataStream(encodedData, QIODevice.ReadOnly) while (not stream.atEnd()): frame = Frame() frame.tileId = stream.readInt() frame.duration = stream.readInt() newFrames.append(frame) elif (data.hasFormat(TILES_MIMETYPE)): encodedData = data.data(TILES_MIMETYPE) stream = QDataStream(encodedData, QIODevice.ReadOnly) while (not stream.atEnd()): frame = Frame() frame.tileId = stream.readInt() frame.duration = FrameListModel.DEFAULT_DURATION newFrames.append(frame) if (newFrames.isEmpty()): return False self.beginInsertRows(QModelIndex(), beginRow, beginRow + newFrames.size() - 1) self.mFrames.insert(beginRow, newFrames.size(), Frame()) for i in range(newFrames.size()): self.mFrames[i + beginRow] = newFrames[i] self.endInsertRows() return True
class BucketFillTool(AbstractTileTool): def tr(self, sourceText, disambiguation = '', n = -1): return QCoreApplication.translate('BucketFillTool', sourceText, disambiguation, n) def __init__(self, parent = None): super().__init__(self.tr("Bucket Fill Tool"), QIcon(":images/22x22/stock-tool-bucket-fill.png"), QKeySequence(self.tr("F")), parent) self.mStamp = TileStamp() self.mFillOverlay = None self.mFillRegion = QRegion() self.mMissingTilesets = QVector() self.mIsActive = False self.mLastShiftStatus = False ## # Indicates if the tool is using the random mode. ## self.mIsRandom = False ## # Contains the value of mIsRandom at that time, when the latest call of # tilePositionChanged() took place. # This variable is needed to detect if the random mode was changed during # mFillOverlay being brushed at an area. ## self.mLastRandomStatus = False ## # Contains all used random cells to use in random mode. # The same cell can be in the list multiple times to make different # random weights possible. ## self.mRandomCellPicker = RandomPicker() def __del__(self): pass def activate(self, scene): super().activate(scene) self.mIsActive = True self.tilePositionChanged(self.tilePosition()) def deactivate(self, scene): super().deactivate(scene) self.mFillRegion = QRegion() self.mIsActive = False def mousePressed(self, event): if (event.button() != Qt.LeftButton or self.mFillRegion.isEmpty()): return if (not self.brushItem().isVisible()): return preview = self.mFillOverlay if not preview: return paint = PaintTileLayer(self.mapDocument(), self.currentTileLayer(), preview.x(), preview.y(), preview) paint.setText(QCoreApplication.translate("Undo Commands", "Fill Area")) if not self.mMissingTilesets.isEmpty(): for tileset in self.mMissingTilesets: AddTileset(self.mapDocument(), tileset, paint) self.mMissingTilesets.clear() fillRegion = QRegion(self.mFillRegion) self.mapDocument().undoStack().push(paint) self.mapDocument().emitRegionEdited(fillRegion, self.currentTileLayer()) def mouseReleased(self, event): pass def modifiersChanged(self, modifiers): # Don't need to recalculate fill region if there was no fill region if (not self.mFillOverlay): return self.tilePositionChanged(self.tilePosition()) def languageChanged(self): self.setName(self.tr("Bucket Fill Tool")) self.setShortcut(QKeySequence(self.tr("F"))) ## # Sets the stamp that is drawn when filling. The BucketFillTool takes # ownership over the stamp layer. ## def setStamp(self, stamp): # Clear any overlay that we presently have with an old stamp self.clearOverlay() self.mStamp = stamp self.updateRandomListAndMissingTilesets() if (self.mIsActive and self.brushItem().isVisible()): self.tilePositionChanged(self.tilePosition()) ## # This returns the actual tile layer which is used to define the current # state. ## def stamp(self): return TileStamp(self.mStamp) def setRandom(self, value): if (self.mIsRandom == value): return self.mIsRandom = value self.updateRandomListAndMissingTilesets() # Don't need to recalculate fill region if there was no fill region if (not self.mFillOverlay): return self.tilePositionChanged(self.tilePosition()) def tilePositionChanged(self, tilePos): # Skip filling if the stamp is empty if self.mStamp.isEmpty(): return # Make sure that a tile layer is selected tileLayer = self.currentTileLayer() if (not tileLayer): return shiftPressed = QApplication.keyboardModifiers() & Qt.ShiftModifier fillRegionChanged = False regionComputer = TilePainter(self.mapDocument(), tileLayer) # If the stamp is a single tile, ignore it when making the region if (not shiftPressed and self.mStamp.variations().size() == 1): variation = self.mStamp.variations().first() stampLayer = variation.tileLayer() if (stampLayer.size() == QSize(1, 1) and stampLayer.cellAt(0, 0) == regionComputer.cellAt(tilePos)): return # This clears the connections so we don't get callbacks self.clearConnections(self.mapDocument()) # Optimization: we don't need to recalculate the fill area # if the new mouse position is still over the filled region # and the shift modifier hasn't changed. if (not self.mFillRegion.contains(tilePos) or shiftPressed != self.mLastShiftStatus): # Clear overlay to make way for a new one self.clearOverlay() # Cache information about how the fill region was created self.mLastShiftStatus = shiftPressed # Get the new fill region if (not shiftPressed): # If not holding shift, a region is generated from the current pos self.mFillRegion = regionComputer.computePaintableFillRegion(tilePos) else: # If holding shift, the region is the selection bounds self.mFillRegion = self.mapDocument().selectedArea() # Fill region is the whole map if there is no selection if (self.mFillRegion.isEmpty()): self.mFillRegion = tileLayer.bounds() # The mouse needs to be in the region if (not self.mFillRegion.contains(tilePos)): self.mFillRegion = QRegion() fillRegionChanged = True # Ensure that a fill region was created before making an overlay layer if (self.mFillRegion.isEmpty()): return if (self.mLastRandomStatus != self.mIsRandom): self.mLastRandomStatus = self.mIsRandom fillRegionChanged = True if (not self.mFillOverlay): # Create a new overlay region fillBounds = self.mFillRegion.boundingRect() self.mFillOverlay = TileLayer(QString(), fillBounds.x(), fillBounds.y(), fillBounds.width(), fillBounds.height()) # Paint the new overlay if (not self.mIsRandom): if (fillRegionChanged or self.mStamp.variations().size() > 1): fillWithStamp(self.mFillOverlay, self.mStamp, self.mFillRegion.translated(-self.mFillOverlay.position())) fillRegionChanged = True else: self.randomFill(self.mFillOverlay, self.mFillRegion) fillRegionChanged = True if (fillRegionChanged): # Update the brush item to draw the overlay self.brushItem().setTileLayer(self.mFillOverlay) # Create connections to know when the overlay should be cleared self.makeConnections() def mapDocumentChanged(self, oldDocument, newDocument): super().mapDocumentChanged(oldDocument, newDocument) self.clearConnections(oldDocument) # Reset things that are probably invalid now if newDocument: self.updateRandomListAndMissingTilesets() self.clearOverlay() def clearOverlay(self): # Clear connections before clearing overlay so there is no # risk of getting a callback and causing an infinite loop self.clearConnections(self.mapDocument()) self.brushItem().clear() self.mFillOverlay = None self.mFillRegion = QRegion() def makeConnections(self): if (not self.mapDocument()): return # Overlay may need to be cleared if a region changed self.mapDocument().regionChanged.connect(self.clearOverlay) # Overlay needs to be cleared if we switch to another layer self.mapDocument().currentLayerIndexChanged.connect(self.clearOverlay) # Overlay needs be cleared if the selection changes, since # the overlay may be bound or may need to be bound to the selection self.mapDocument().selectedAreaChanged.connect(self.clearOverlay) def clearConnections(self, mapDocument): if (not mapDocument): return try: mapDocument.regionChanged.disconnect(self.clearOverlay) mapDocument.currentLayerIndexChanged.disconnect(self.clearOverlay) mapDocument.selectedAreaChanged.disconnect(self.clearOverlay) except: pass ## # Updates the list of random cells. # This is done by taking all non-null tiles from the original stamp mStamp. ## def updateRandomListAndMissingTilesets(self): self.mRandomCellPicker.clear() self.mMissingTilesets.clear() for variation in self.mStamp.variations(): self.mapDocument().unifyTilesets(variation.map, self.mMissingTilesets) if self.mIsRandom: for cell in variation.tileLayer(): if not cell.isEmpty(): self.mRandomCellPicker.add(cell, cell.tile.probability()) ## # Fills the given \a region in the given \a tileLayer with random tiles. ## def randomFill(self, tileLayer, region): if (region.isEmpty() or self.mRandomList.empty()): return for rect in region.translated(-tileLayer.position()).rects(): for _x in range(rect.left(), rect.right()+1): for _y in range(rect.top(), rect.bottom()+1): tileLayer.setCell(_x, _y, self.mRandomCellPicker.pick())
class BucketFillTool(AbstractTileTool): def tr(self, sourceText, disambiguation='', n=-1): return QCoreApplication.translate('BucketFillTool', sourceText, disambiguation, n) def __init__(self, parent=None): super().__init__(self.tr("Bucket Fill Tool"), QIcon(":images/22x22/stock-tool-bucket-fill.png"), QKeySequence(self.tr("F")), parent) self.mStamp = TileStamp() self.mFillOverlay = None self.mFillRegion = QRegion() self.mMissingTilesets = QVector() self.mIsActive = False self.mLastShiftStatus = False ## # Indicates if the tool is using the random mode. ## self.mIsRandom = False ## # Contains the value of mIsRandom at that time, when the latest call of # tilePositionChanged() took place. # This variable is needed to detect if the random mode was changed during # mFillOverlay being brushed at an area. ## self.mLastRandomStatus = False ## # Contains all used random cells to use in random mode. # The same cell can be in the list multiple times to make different # random weights possible. ## self.mRandomCellPicker = RandomPicker() def __del__(self): pass def activate(self, scene): super().activate(scene) self.mIsActive = True self.tilePositionChanged(self.tilePosition()) def deactivate(self, scene): super().deactivate(scene) self.mFillRegion = QRegion() self.mIsActive = False def mousePressed(self, event): if (event.button() != Qt.LeftButton or self.mFillRegion.isEmpty()): return if (not self.brushItem().isVisible()): return preview = self.mFillOverlay if not preview: return paint = PaintTileLayer(self.mapDocument(), self.currentTileLayer(), preview.x(), preview.y(), preview) paint.setText(QCoreApplication.translate("Undo Commands", "Fill Area")) if not self.mMissingTilesets.isEmpty(): for tileset in self.mMissingTilesets: AddTileset(self.mapDocument(), tileset, paint) self.mMissingTilesets.clear() fillRegion = QRegion(self.mFillRegion) self.mapDocument().undoStack().push(paint) self.mapDocument().emitRegionEdited(fillRegion, self.currentTileLayer()) def mouseReleased(self, event): pass def modifiersChanged(self, modifiers): # Don't need to recalculate fill region if there was no fill region if (not self.mFillOverlay): return self.tilePositionChanged(self.tilePosition()) def languageChanged(self): self.setName(self.tr("Bucket Fill Tool")) self.setShortcut(QKeySequence(self.tr("F"))) ## # Sets the stamp that is drawn when filling. The BucketFillTool takes # ownership over the stamp layer. ## def setStamp(self, stamp): # Clear any overlay that we presently have with an old stamp self.clearOverlay() self.mStamp = stamp self.updateRandomListAndMissingTilesets() if (self.mIsActive and self.brushItem().isVisible()): self.tilePositionChanged(self.tilePosition()) ## # This returns the actual tile layer which is used to define the current # state. ## def stamp(self): return TileStamp(self.mStamp) def setRandom(self, value): if (self.mIsRandom == value): return self.mIsRandom = value self.updateRandomListAndMissingTilesets() # Don't need to recalculate fill region if there was no fill region if (not self.mFillOverlay): return self.tilePositionChanged(self.tilePosition()) def tilePositionChanged(self, tilePos): # Skip filling if the stamp is empty if self.mStamp.isEmpty(): return # Make sure that a tile layer is selected tileLayer = self.currentTileLayer() if (not tileLayer): return shiftPressed = QApplication.keyboardModifiers() & Qt.ShiftModifier fillRegionChanged = False regionComputer = TilePainter(self.mapDocument(), tileLayer) # If the stamp is a single tile, ignore it when making the region if (not shiftPressed and self.mStamp.variations().size() == 1): variation = self.mStamp.variations().first() stampLayer = variation.tileLayer() if (stampLayer.size() == QSize(1, 1) and stampLayer.cellAt(0, 0) == regionComputer.cellAt(tilePos)): return # This clears the connections so we don't get callbacks self.clearConnections(self.mapDocument()) # Optimization: we don't need to recalculate the fill area # if the new mouse position is still over the filled region # and the shift modifier hasn't changed. if (not self.mFillRegion.contains(tilePos) or shiftPressed != self.mLastShiftStatus): # Clear overlay to make way for a new one self.clearOverlay() # Cache information about how the fill region was created self.mLastShiftStatus = shiftPressed # Get the new fill region if (not shiftPressed): # If not holding shift, a region is generated from the current pos self.mFillRegion = regionComputer.computePaintableFillRegion( tilePos) else: # If holding shift, the region is the selection bounds self.mFillRegion = self.mapDocument().selectedArea() # Fill region is the whole map if there is no selection if (self.mFillRegion.isEmpty()): self.mFillRegion = tileLayer.bounds() # The mouse needs to be in the region if (not self.mFillRegion.contains(tilePos)): self.mFillRegion = QRegion() fillRegionChanged = True # Ensure that a fill region was created before making an overlay layer if (self.mFillRegion.isEmpty()): return if (self.mLastRandomStatus != self.mIsRandom): self.mLastRandomStatus = self.mIsRandom fillRegionChanged = True if (not self.mFillOverlay): # Create a new overlay region fillBounds = self.mFillRegion.boundingRect() self.mFillOverlay = TileLayer(QString(), fillBounds.x(), fillBounds.y(), fillBounds.width(), fillBounds.height()) # Paint the new overlay if (not self.mIsRandom): if (fillRegionChanged or self.mStamp.variations().size() > 1): fillWithStamp( self.mFillOverlay, self.mStamp, self.mFillRegion.translated(-self.mFillOverlay.position())) fillRegionChanged = True else: self.randomFill(self.mFillOverlay, self.mFillRegion) fillRegionChanged = True if (fillRegionChanged): # Update the brush item to draw the overlay self.brushItem().setTileLayer(self.mFillOverlay) # Create connections to know when the overlay should be cleared self.makeConnections() def mapDocumentChanged(self, oldDocument, newDocument): super().mapDocumentChanged(oldDocument, newDocument) self.clearConnections(oldDocument) # Reset things that are probably invalid now if newDocument: self.updateRandomListAndMissingTilesets() self.clearOverlay() def clearOverlay(self): # Clear connections before clearing overlay so there is no # risk of getting a callback and causing an infinite loop self.clearConnections(self.mapDocument()) self.brushItem().clear() self.mFillOverlay = None self.mFillRegion = QRegion() def makeConnections(self): if (not self.mapDocument()): return # Overlay may need to be cleared if a region changed self.mapDocument().regionChanged.connect(self.clearOverlay) # Overlay needs to be cleared if we switch to another layer self.mapDocument().currentLayerIndexChanged.connect(self.clearOverlay) # Overlay needs be cleared if the selection changes, since # the overlay may be bound or may need to be bound to the selection self.mapDocument().selectedAreaChanged.connect(self.clearOverlay) def clearConnections(self, mapDocument): if (not mapDocument): return try: mapDocument.regionChanged.disconnect(self.clearOverlay) mapDocument.currentLayerIndexChanged.disconnect(self.clearOverlay) mapDocument.selectedAreaChanged.disconnect(self.clearOverlay) except: pass ## # Updates the list of random cells. # This is done by taking all non-null tiles from the original stamp mStamp. ## def updateRandomListAndMissingTilesets(self): self.mRandomCellPicker.clear() self.mMissingTilesets.clear() for variation in self.mStamp.variations(): self.mapDocument().unifyTilesets(variation.map, self.mMissingTilesets) if self.mIsRandom: for cell in variation.tileLayer(): if not cell.isEmpty(): self.mRandomCellPicker.add(cell, cell.tile.probability()) ## # Fills the given \a region in the given \a tileLayer with random tiles. ## def randomFill(self, tileLayer, region): if (region.isEmpty() or self.mRandomList.empty()): return for rect in region.translated(-tileLayer.position()).rects(): for _x in range(rect.left(), rect.right() + 1): for _y in range(rect.top(), rect.bottom() + 1): tileLayer.setCell(_x, _y, self.mRandomCellPicker.pick())
class StampBrush(AbstractTileTool): ## # Emitted when the currently selected tiles changed. The stamp brush emits # this signal instead of setting its stamp directly so that the fill tool # also gets the new stamp. ## currentTilesChanged = pyqtSignal(list) ## # Emitted when a stamp was captured from the map. The stamp brush emits # this signal instead of setting its stamp directly so that the fill tool # also gets the new stamp. ## stampCaptured = pyqtSignal(TileStamp) def __init__(self, parent = None): super().__init__(self.tr("Stamp Brush"), QIcon(":images/22x22/stock-tool-clone.png"), QKeySequence(self.tr("B")), parent) ## # This stores the current behavior. ## self.mBrushBehavior = BrushBehavior.Free self.mIsRandom = False self.mCaptureStart = QPoint() self.mRandomCellPicker = RandomPicker() ## # mStamp is a tile layer in which is the selection the user made # either by rightclicking (Capture) or at the tilesetdock ## self.mStamp = TileStamp() self.mPreviewLayer = None self.mMissingTilesets = QVector() self.mPrevTilePosition = QPoint() self.mStampReference = QPoint() def __del__(self): pass def tr(self, sourceText, disambiguation = '', n = -1): return QCoreApplication.translate('StampBrush', sourceText, disambiguation, n) def mousePressed(self, event): if (not self.brushItem().isVisible()): return if (event.button() == Qt.LeftButton): x = self.mBrushBehavior if x==BrushBehavior.Line: self.mStampReference = self.tilePosition() self.mBrushBehavior = BrushBehavior.LineStartSet elif x==BrushBehavior.Circle: self.mStampReference = self.tilePosition() self.mBrushBehavior = BrushBehavior.CircleMidSet elif x==BrushBehavior.LineStartSet: self.doPaint() self.mStampReference = self.tilePosition() elif x==BrushBehavior.CircleMidSet: self.doPaint() elif x==BrushBehavior.Paint: self.beginPaint() elif x==BrushBehavior.Free: self.beginPaint() self.mBrushBehavior = BrushBehavior.Paint elif x==BrushBehavior.Capture: pass else: if (event.button() == Qt.RightButton): self.beginCapture() def mouseReleased(self, event): x = self.mBrushBehavior if x==BrushBehavior.Capture: if (event.button() == Qt.RightButton): self.endCapture() self.mBrushBehavior = BrushBehavior.Free elif x==BrushBehavior.Paint: if (event.button() == Qt.LeftButton): self.mBrushBehavior = BrushBehavior.Free # allow going over different variations by repeatedly clicking self.updatePreview() else: # do nothing? pass def modifiersChanged(self, modifiers): if self.mStamp.isEmpty(): return if (modifiers & Qt.ShiftModifier): if (modifiers & Qt.ControlModifier): if self.mBrushBehavior == BrushBehavior.LineStartSet: self.mBrushBehavior = BrushBehavior.CircleMidSet else: self.mBrushBehavior = BrushBehavior.Circle else: if self.mBrushBehavior == BrushBehavior.CircleMidSet: self.mBrushBehavior = BrushBehavior.LineStartSet else: self.mBrushBehavior = BrushBehavior.Line else: self.mBrushBehavior = BrushBehavior.Free self.updatePreview() def languageChanged(self): self.setName(self.tr("Stamp Brush")) self.setShortcut(QKeySequence(self.tr("B"))) ## # Sets the stamp that is drawn when painting. The stamp brush takes # ownership over the stamp layer. ## def setStamp(self, stamp): if (self.mStamp == stamp): return self.mStamp = stamp self.updateRandomList() self.updatePreview() ## # This returns the current tile stamp used for painting. ## def stamp(self): return self.mStamp def setRandom(self, value): if self.mIsRandom == value: return self.mIsRandom = value self.updateRandomList() self.updatePreview() def tilePositionChanged(self, pos): x = self.mBrushBehavior if x==BrushBehavior.Paint: # Draw a line from the previous point to avoid gaps, skipping the # first point, since it was painted when the mouse was pressed, or the # last time the mouse was moved. points = pointsOnLine(self.mPrevTilePosition, pos) editedRegion = QRegion() ptSize = points.size() ptLast = ptSize - 1 for i in range(1, ptSize): self.drawPreviewLayer(QVector(points[i])) # Only update the brush item for the last drawn piece if i == ptLast: self.brushItem().setTileLayer(self.mPreviewLayer) editedRegion |= self.doPaint(PaintFlags.Mergeable | PaintFlags.SuppressRegionEdited) self.mapDocument().emitRegionEdited(editedRegion, self.currentTileLayer()) else: self.updatePreview() self.mPrevTilePosition = pos def mapDocumentChanged(self, oldDocument, newDocument): super().mapDocumentChanged(oldDocument, newDocument) if newDocument: self.updateRandomList() self.updatePreview() def beginPaint(self): if (self.mBrushBehavior != BrushBehavior.Free): return self.mBrushBehavior = BrushBehavior.Paint self.doPaint() ## # Merges the tile layer of its brush item into the current map. # # \a flags can be set to Mergeable, which applies to the undo command. # # \a offsetX and \a offsetY give an offset where to merge the brush items tile # layer into the current map. # # Returns the edited region. ## def doPaint(self, flags = 0): preview = self.mPreviewLayer if not preview: return QRegion() # This method shouldn't be called when current layer is not a tile layer tileLayer = self.currentTileLayer() if (not tileLayer.bounds().intersects(QRect(preview.x(), preview.y(), preview.width(), preview.height()))): return QRegion() paint = PaintTileLayer(self.mapDocument(), tileLayer, preview.x(), preview.y(), preview) if not self.mMissingTilesets.isEmpty(): for tileset in self.mMissingTilesets: AddTileset(self.mapDocument(), tileset, paint) self.mMissingTilesets.clear() paint.setMergeable(flags & PaintFlags.Mergeable) self.mapDocument().undoStack().push(paint) editedRegion = preview.region() if (not (flags & PaintFlags.SuppressRegionEdited)): self.mapDocument().emitRegionEdited(editedRegion, tileLayer) return editedRegion def beginCapture(self): if (self.mBrushBehavior != BrushBehavior.Free): return self.mBrushBehavior = BrushBehavior.Capture self.mCaptureStart = self.tilePosition() self.setStamp(TileStamp()) def endCapture(self): if (self.mBrushBehavior != BrushBehavior.Capture): return self.mBrushBehavior = BrushBehavior.Free tileLayer = self.currentTileLayer() # Intersect with the layer and translate to layer coordinates captured = self.capturedArea() captured &= QRect(tileLayer.x(), tileLayer.y(), tileLayer.width(), tileLayer.height()) if (captured.isValid()): captured.translate(-tileLayer.x(), -tileLayer.y()) map = tileLayer.map() capture = tileLayer.copy(captured) stamp = Map(map.orientation(), capture.width(), capture.height(), map.tileWidth(), map.tileHeight()) # Add tileset references to map for tileset in capture.usedTilesets(): stamp.addTileset(tileset) stamp.addLayer(capture) self.stampCaptured.emit(TileStamp(stamp)) else: self.updatePreview() def capturedArea(self): captured = QRect(self.mCaptureStart, self.tilePosition()).normalized() if (captured.width() == 0): captured.adjust(-1, 0, 1, 0) if (captured.height() == 0): captured.adjust(0, -1, 0, 1) return captured ## # Updates the position of the brush item. ## def updatePreview(self, *args): l = len(args) if l==0: ## # Updates the position of the brush item based on the mouse position. ## self.updatePreview(self.tilePosition()) elif l==1: tilePos = args[0] if not self.mapDocument(): return tileRegion = QRegion() if self.mBrushBehavior == BrushBehavior.Capture: self.mPreviewLayer = None tileRegion = self.capturedArea() elif self.mStamp.isEmpty(): self.mPreviewLayer = None tileRegion = QRect(tilePos, QSize(1, 1)) else: if self.mBrushBehavior == BrushBehavior.LineStartSet: self.drawPreviewLayer(pointsOnLine(self.mStampReference, tilePos)) elif self.mBrushBehavior == BrushBehavior.CircleMidSet: self.drawPreviewLayer(pointsOnEllipse(self.mStampReference, tilePos)) elif self.mBrushBehavior == BrushBehavior.Capture: # already handled above pass elif self.mBrushBehavior == BrushBehavior.Circle: # while finding the mid point, there is no need to show # the (maybe bigger than 1x1) stamp self.mPreviewLayer.clear() tileRegion = QRect(tilePos, QSize(1, 1)) elif self.mBrushBehavior==BrushBehavior.Line or self.mBrushBehavior==BrushBehavior.Free or self.mBrushBehavior==BrushBehavior.Paint: self.drawPreviewLayer(QVector(tilePos)) self.brushItem().setTileLayer(self.mPreviewLayer) if not tileRegion.isEmpty(): self.brushItem().setTileRegion(tileRegion) ## # Updates the list used random stamps. # This is done by taking all non-null tiles from the original stamp mStamp. ## def updateRandomList(self): self.mRandomCellPicker.clear() if not self.mIsRandom: return self.mMissingTilesets.clear() for variation in self.mStamp.variations(): self.mapDocument().unifyTilesets(variation.map, self.mMissingTilesets) tileLayer = variation.tileLayer() for x in range(tileLayer.width()): for y in range(tileLayer.height()): cell = tileLayer.cellAt(x, y) if not cell.isEmpty(): self.mRandomCellPicker.add(cell, cell.tile.probability()) ## # Draws the preview layer. # It tries to put at all given points a stamp of the current stamp at the # corresponding position. # It also takes care, that no overlaps appear. # So it will check for every point if it can place a stamp there without # overlap. ## def drawPreviewLayer(self, _list): self.mPreviewLayer = None if self.mStamp.isEmpty(): return if self.mIsRandom: if self.mRandomCellPicker.isEmpty(): return paintedRegion = QRegion() for p in _list: paintedRegion += QRect(p, QSize(1, 1)) bounds = paintedRegion.boundingRect() preview = TileLayer(QString(), bounds.x(), bounds.y(), bounds.width(), bounds.height()) for p in _list: cell = self.mRandomCellPicker.pick() preview.setCell(p.x() - bounds.left(), p.y() - bounds.top(), cell) self.mPreviewLayer = preview else: self.mMissingTilesets.clear() paintedRegion = QRegion() operations = QVector() regionCache = QHash() for p in _list: variation = self.mStamp.randomVariation() self.mapDocument().unifyTilesets(variation.map, self.mMissingTilesets) stamp = variation.tileLayer() stampRegion = QRegion() if regionCache.contains(stamp): stampRegion = regionCache.value(stamp) else: stampRegion = stamp.region() regionCache.insert(stamp, stampRegion) centered = QPoint(p.x() - int(stamp.width() / 2), p.y() - int(stamp.height() / 2)) region = stampRegion.translated(centered.x(), centered.y()) if not paintedRegion.intersects(region): paintedRegion += region op = PaintOperation(centered, stamp) operations.append(op) bounds = paintedRegion.boundingRect() preview = TileLayer(QString(), bounds.x(), bounds.y(), bounds.width(), bounds.height()) for op in operations: preview.merge(op.pos - bounds.topLeft(), op.stamp) self.mPreviewLayer = preview
class Tile(Object): def __init__(self, *args): super().__init__(Object.TileType) l = len(args) if l==3: image, id, tileset = args self.mImageSource = QString() elif l==4: image, imageSource, id, tileset = args self.mImageSource = imageSource self.mId = id self.mTileset = tileset self.mImage = image self.mTerrain = 0xffffffff self.mProbability = 1.0 self.mObjectGroup = None self.mFrames = QVector() self.mCurrentFrameIndex = 0 self.mUnusedTime = 0 def __del__(self): del self.mObjectGroup ## # Returns the tileset that this tile is part of as a shared pointer. ## def sharedTileset(self): return self.mTileset.sharedPointer() ## # Returns ID of this tile within its tileset. ## def id(self): return self.mId ## # Returns the tileset that this tile is part of. ## def tileset(self): return self.mTileset ## # Returns the image of this tile. ## def image(self): return QPixmap(self.mImage) ## # Returns the image for rendering this tile, taking into account tile # animations. ## def currentFrameImage(self): if (self.isAnimated()): frame = self.mFrames.at(self.mCurrentFrameIndex) return self.mTileset.tileAt(frame.tileId).image() else: return QPixmap(self.mImage) ## # Returns the drawing offset of the tile (in pixels). ## def offset(self): return self.mTileset.tileOffset() ## # Sets the image of this tile. ## def setImage(self, image): self.mImage = image ## # Returns the file name of the external image that represents this tile. # When this tile doesn't refer to an external image, an empty string is # returned. ## def imageSource(self): return self.mImageSource ## # Returns the file name of the external image that represents this tile. # When this tile doesn't refer to an external image, an empty string is # returned. ## def setImageSource(self, imageSource): self.mImageSource = imageSource ## # Returns the width of this tile. ## def width(self): return self.mImage.width() ## # Returns the height of this tile. ## def height(self): return self.mImage.height() ## # Returns the size of this tile. ## def size(self): return self.mImage.size() ## # Returns the Terrain of a given corner. ## def terrainAtCorner(self, corner): return self.mTileset.terrain(self.cornerTerrainId(corner)) ## # Returns the terrain id at a given corner. ## def cornerTerrainId(self, corner): t = (self.terrain() >> (3 - corner)*8) & 0xFF if t == 0xFF: return -1 return t ## # Set the terrain type of a given corner. ## def setCornerTerrainId(self, corner, terrainId): self.setTerrain(setTerrainCorner(self.mTerrain, corner, terrainId)) ## # Returns the terrain for each corner of this tile. ## def terrain(self): return self.mTerrain ## # Set the terrain for each corner of the tile. ## def setTerrain(self, terrain): if (self.mTerrain == terrain): return self.mTerrain = terrain self.mTileset.markTerrainDistancesDirty() ## # Returns the probability of this terrain type appearing while painting (0-100%). ## def probability(self): return self.mProbability ## # Set the relative probability of this tile appearing while painting. ## def setProbability(self, probability): self.mProbability = probability ## # @return The group of objects associated with this tile. This is generally # expected to be used for editing collision shapes. ## def objectGroup(self): return self.mObjectGroup ## # Sets \a objectGroup to be the group of objects associated with this tile. # The Tile takes ownership over the ObjectGroup and it can't also be part of # a map. ## def setObjectGroup(self, objectGroup): if (self.mObjectGroup == objectGroup): return del self.mObjectGroup self.mObjectGroup = objectGroup ## # Swaps the object group of this tile with \a objectGroup. The tile releases # ownership over its existing object group and takes ownership over the new # one. # # @return The previous object group referenced by this tile. ## def swapObjectGroup(self, objectGroup): previousObjectGroup = self.mObjectGroup self.mObjectGroup = objectGroup return previousObjectGroup def frames(self): return self.mFrames ## # Sets the animation frames to be used by this tile. Resets any currently # running animation. ## def setFrames(self, frames): self.mFrames = frames self.mCurrentFrameIndex = 0 self.mUnusedTime = 0 def isAnimated(self): return not self.mFrames.isEmpty() def currentFrameIndex(self): return self.mCurrentFrameIndex ## # Advances this tile animation by the given amount of milliseconds. Returns # whether this caused the current tileId to change. ## def advanceAnimation(self, ms): if (not self.isAnimated()): return False self.mUnusedTime += ms frame = self.mFrames.at(self.mCurrentFrameIndex) previousTileId = frame.tileId while (frame.duration > 0 and self.mUnusedTime > frame.duration): self.mUnusedTime -= frame.duration self.mCurrentFrameIndex = (self.mCurrentFrameIndex + 1) % self.mFrames.size() frame = self.mFrames.at(self.mCurrentFrameIndex) return previousTileId != frame.tileId
class StampBrush(AbstractTileTool): ## # Emitted when the currently selected tiles changed. The stamp brush emits # this signal instead of setting its stamp directly so that the fill tool # also gets the new stamp. ## currentTilesChanged = pyqtSignal(list) ## # Emitted when a stamp was captured from the map. The stamp brush emits # this signal instead of setting its stamp directly so that the fill tool # also gets the new stamp. ## stampCaptured = pyqtSignal(TileStamp) def __init__(self, parent=None): super().__init__(self.tr("Stamp Brush"), QIcon(":images/22x22/stock-tool-clone.png"), QKeySequence(self.tr("B")), parent) ## # This stores the current behavior. ## self.mBrushBehavior = BrushBehavior.Free self.mIsRandom = False self.mCaptureStart = QPoint() self.mRandomCellPicker = RandomPicker() ## # mStamp is a tile layer in which is the selection the user made # either by rightclicking (Capture) or at the tilesetdock ## self.mStamp = TileStamp() self.mPreviewLayer = None self.mMissingTilesets = QVector() self.mPrevTilePosition = QPoint() self.mStampReference = QPoint() def __del__(self): pass def tr(self, sourceText, disambiguation='', n=-1): return QCoreApplication.translate('StampBrush', sourceText, disambiguation, n) def mousePressed(self, event): if (not self.brushItem().isVisible()): return if (event.button() == Qt.LeftButton): x = self.mBrushBehavior if x == BrushBehavior.Line: self.mStampReference = self.tilePosition() self.mBrushBehavior = BrushBehavior.LineStartSet elif x == BrushBehavior.Circle: self.mStampReference = self.tilePosition() self.mBrushBehavior = BrushBehavior.CircleMidSet elif x == BrushBehavior.LineStartSet: self.doPaint() self.mStampReference = self.tilePosition() elif x == BrushBehavior.CircleMidSet: self.doPaint() elif x == BrushBehavior.Paint: self.beginPaint() elif x == BrushBehavior.Free: self.beginPaint() self.mBrushBehavior = BrushBehavior.Paint elif x == BrushBehavior.Capture: pass else: if (event.button() == Qt.RightButton): self.beginCapture() def mouseReleased(self, event): x = self.mBrushBehavior if x == BrushBehavior.Capture: if (event.button() == Qt.RightButton): self.endCapture() self.mBrushBehavior = BrushBehavior.Free elif x == BrushBehavior.Paint: if (event.button() == Qt.LeftButton): self.mBrushBehavior = BrushBehavior.Free # allow going over different variations by repeatedly clicking self.updatePreview() else: # do nothing? pass def modifiersChanged(self, modifiers): if self.mStamp.isEmpty(): return if (modifiers & Qt.ShiftModifier): if (modifiers & Qt.ControlModifier): if self.mBrushBehavior == BrushBehavior.LineStartSet: self.mBrushBehavior = BrushBehavior.CircleMidSet else: self.mBrushBehavior = BrushBehavior.Circle else: if self.mBrushBehavior == BrushBehavior.CircleMidSet: self.mBrushBehavior = BrushBehavior.LineStartSet else: self.mBrushBehavior = BrushBehavior.Line else: self.mBrushBehavior = BrushBehavior.Free self.updatePreview() def languageChanged(self): self.setName(self.tr("Stamp Brush")) self.setShortcut(QKeySequence(self.tr("B"))) ## # Sets the stamp that is drawn when painting. The stamp brush takes # ownership over the stamp layer. ## def setStamp(self, stamp): if (self.mStamp == stamp): return self.mStamp = stamp self.updateRandomList() self.updatePreview() ## # This returns the current tile stamp used for painting. ## def stamp(self): return self.mStamp def setRandom(self, value): if self.mIsRandom == value: return self.mIsRandom = value self.updateRandomList() self.updatePreview() def tilePositionChanged(self, pos): x = self.mBrushBehavior if x == BrushBehavior.Paint: # Draw a line from the previous point to avoid gaps, skipping the # first point, since it was painted when the mouse was pressed, or the # last time the mouse was moved. points = pointsOnLine(self.mPrevTilePosition, pos) editedRegion = QRegion() ptSize = points.size() ptLast = ptSize - 1 for i in range(1, ptSize): self.drawPreviewLayer(QVector(points[i])) # Only update the brush item for the last drawn piece if i == ptLast: self.brushItem().setTileLayer(self.mPreviewLayer) editedRegion |= self.doPaint(PaintFlags.Mergeable | PaintFlags.SuppressRegionEdited) self.mapDocument().emitRegionEdited(editedRegion, self.currentTileLayer()) else: self.updatePreview() self.mPrevTilePosition = pos def mapDocumentChanged(self, oldDocument, newDocument): super().mapDocumentChanged(oldDocument, newDocument) if newDocument: self.updateRandomList() self.updatePreview() def beginPaint(self): if (self.mBrushBehavior != BrushBehavior.Free): return self.mBrushBehavior = BrushBehavior.Paint self.doPaint() ## # Merges the tile layer of its brush item into the current map. # # \a flags can be set to Mergeable, which applies to the undo command. # # \a offsetX and \a offsetY give an offset where to merge the brush items tile # layer into the current map. # # Returns the edited region. ## def doPaint(self, flags=0): preview = self.mPreviewLayer if not preview: return QRegion() # This method shouldn't be called when current layer is not a tile layer tileLayer = self.currentTileLayer() if (not tileLayer.bounds().intersects( QRect(preview.x(), preview.y(), preview.width(), preview.height()))): return QRegion() paint = PaintTileLayer(self.mapDocument(), tileLayer, preview.x(), preview.y(), preview) if not self.mMissingTilesets.isEmpty(): for tileset in self.mMissingTilesets: AddTileset(self.mapDocument(), tileset, paint) self.mMissingTilesets.clear() paint.setMergeable(flags & PaintFlags.Mergeable) self.mapDocument().undoStack().push(paint) editedRegion = preview.region() if (not (flags & PaintFlags.SuppressRegionEdited)): self.mapDocument().emitRegionEdited(editedRegion, tileLayer) return editedRegion def beginCapture(self): if (self.mBrushBehavior != BrushBehavior.Free): return self.mBrushBehavior = BrushBehavior.Capture self.mCaptureStart = self.tilePosition() self.setStamp(TileStamp()) def endCapture(self): if (self.mBrushBehavior != BrushBehavior.Capture): return self.mBrushBehavior = BrushBehavior.Free tileLayer = self.currentTileLayer() # Intersect with the layer and translate to layer coordinates captured = self.capturedArea() captured &= QRect(tileLayer.x(), tileLayer.y(), tileLayer.width(), tileLayer.height()) if (captured.isValid()): captured.translate(-tileLayer.x(), -tileLayer.y()) map = tileLayer.map() capture = tileLayer.copy(captured) stamp = Map(map.orientation(), capture.width(), capture.height(), map.tileWidth(), map.tileHeight()) # Add tileset references to map for tileset in capture.usedTilesets(): stamp.addTileset(tileset) stamp.addLayer(capture) self.stampCaptured.emit(TileStamp(stamp)) else: self.updatePreview() def capturedArea(self): captured = QRect(self.mCaptureStart, self.tilePosition()).normalized() if (captured.width() == 0): captured.adjust(-1, 0, 1, 0) if (captured.height() == 0): captured.adjust(0, -1, 0, 1) return captured ## # Updates the position of the brush item. ## def updatePreview(self, *args): l = len(args) if l == 0: ## # Updates the position of the brush item based on the mouse position. ## self.updatePreview(self.tilePosition()) elif l == 1: tilePos = args[0] if not self.mapDocument(): return tileRegion = QRegion() if self.mBrushBehavior == BrushBehavior.Capture: self.mPreviewLayer = None tileRegion = self.capturedArea() elif self.mStamp.isEmpty(): self.mPreviewLayer = None tileRegion = QRect(tilePos, QSize(1, 1)) else: if self.mBrushBehavior == BrushBehavior.LineStartSet: self.drawPreviewLayer( pointsOnLine(self.mStampReference, tilePos)) elif self.mBrushBehavior == BrushBehavior.CircleMidSet: self.drawPreviewLayer( pointsOnEllipse(self.mStampReference, tilePos)) elif self.mBrushBehavior == BrushBehavior.Capture: # already handled above pass elif self.mBrushBehavior == BrushBehavior.Circle: # while finding the mid point, there is no need to show # the (maybe bigger than 1x1) stamp self.mPreviewLayer.clear() tileRegion = QRect(tilePos, QSize(1, 1)) elif self.mBrushBehavior == BrushBehavior.Line or self.mBrushBehavior == BrushBehavior.Free or self.mBrushBehavior == BrushBehavior.Paint: self.drawPreviewLayer(QVector(tilePos)) self.brushItem().setTileLayer(self.mPreviewLayer) if not tileRegion.isEmpty(): self.brushItem().setTileRegion(tileRegion) ## # Updates the list used random stamps. # This is done by taking all non-null tiles from the original stamp mStamp. ## def updateRandomList(self): self.mRandomCellPicker.clear() if not self.mIsRandom: return self.mMissingTilesets.clear() for variation in self.mStamp.variations(): self.mapDocument().unifyTilesets(variation.map, self.mMissingTilesets) tileLayer = variation.tileLayer() for x in range(tileLayer.width()): for y in range(tileLayer.height()): cell = tileLayer.cellAt(x, y) if not cell.isEmpty(): self.mRandomCellPicker.add(cell, cell.tile.probability()) ## # Draws the preview layer. # It tries to put at all given points a stamp of the current stamp at the # corresponding position. # It also takes care, that no overlaps appear. # So it will check for every point if it can place a stamp there without # overlap. ## def drawPreviewLayer(self, _list): self.mPreviewLayer = None if self.mStamp.isEmpty(): return if self.mIsRandom: if self.mRandomCellPicker.isEmpty(): return paintedRegion = QRegion() for p in _list: paintedRegion += QRect(p, QSize(1, 1)) bounds = paintedRegion.boundingRect() preview = TileLayer(QString(), bounds.x(), bounds.y(), bounds.width(), bounds.height()) for p in _list: cell = self.mRandomCellPicker.pick() preview.setCell(p.x() - bounds.left(), p.y() - bounds.top(), cell) self.mPreviewLayer = preview else: self.mMissingTilesets.clear() paintedRegion = QRegion() operations = QVector() regionCache = QHash() for p in _list: variation = self.mStamp.randomVariation() self.mapDocument().unifyTilesets(variation.map, self.mMissingTilesets) stamp = variation.tileLayer() stampRegion = QRegion() if regionCache.contains(stamp): stampRegion = regionCache.value(stamp) else: stampRegion = stamp.region() regionCache.insert(stamp, stampRegion) centered = QPoint(p.x() - int(stamp.width() / 2), p.y() - int(stamp.height() / 2)) region = stampRegion.translated(centered.x(), centered.y()) if not paintedRegion.intersects(region): paintedRegion += region op = PaintOperation(centered, stamp) operations.append(op) bounds = paintedRegion.boundingRect() preview = TileLayer(QString(), bounds.x(), bounds.y(), bounds.width(), bounds.height()) for op in operations: preview.merge(op.pos - bounds.topLeft(), op.stamp) self.mPreviewLayer = preview