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 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