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 AutomappingManager(QObject): ## # This signal is emited after automapping was done and an error occurred. ## errorsOccurred = pyqtSignal(bool) ## # This signal is emited after automapping was done and a warning occurred. ## warningsOccurred = pyqtSignal(bool) ## # Constructor. ## def __init__(self, parent = None): super().__init__(parent) ## # The current map document. ## self.mMapDocument = None ## # For each new file of rules a new AutoMapper is setup. In this vector we # can store all of the AutoMappers in order. ## self.mAutoMappers = QVector() ## # This tells you if the rules for the current map document were already # loaded. ## self.mLoaded = False ## # Contains all errors which occurred until canceling. # If mError is not empty, no serious result can be expected. ## self.mError = '' ## # Contains all strings, which try to explain unusual and unexpected # behavior. ## self.mWarning = QString() def __del__(self): self.cleanUp() def setMapDocument(self, mapDocument): self.cleanUp() if (self.mMapDocument): self.mMapDocument.disconnect() self.mMapDocument = mapDocument if (self.mMapDocument): self.mMapDocument.regionEdited.connect(self.autoMap) self.mLoaded = False def errorString(self): return self.mError def warningString(self): return self.mWarning ## # This triggers an automapping on the whole current map document. ## def autoMap(self, *args): l = len(args) if l == 0: if (not self.mMapDocument): return map = self.mMapDocument.Map() w = map.width() h = map.height() self.autoMapInternal(QRect(0, 0, w, h), None) elif l==2: where, touchedLayer = args if (preferences.Preferences.instance().automappingDrawing()): self.autoMapInternal(where, touchedLayer) ## # This function parses a rules file. # For each path which is a rule, (fileextension is tmx) an AutoMapper # object is setup. # # If a fileextension is txt, this file will be opened and searched for # rules again. # # @return if the loading was successful: return True if it suceeded. ## def loadFile(self, filePath): ret = True absPath = QFileInfo(filePath).path() rulesFile = QFile(filePath) if (not rulesFile.exists()): self.mError += self.tr("No rules file found at:\n%s\n"%filePath) return False if (not rulesFile.open(QIODevice.ReadOnly | QIODevice.Text)): self.mError += self.tr("Error opening rules file:\n%s\n"%filePath) return False i = QTextStream(rulesFile) line = ' ' while line != '': line = i.readLine() rulePath = line.strip() if (rulePath=='' or rulePath.startswith('#') or rulePath.startswith("//")): continue if (QFileInfo(rulePath).isRelative()): rulePath = absPath + '/' + rulePath if (not QFileInfo(rulePath).exists()): self.mError += self.tr("File not found:\n%s"%rulePath) + '\n' ret = False continue if (rulePath.lower().endswith(".tmx")): tmxFormat = TmxMapFormat() rules = tmxFormat.read(rulePath) if (not rules): self.mError += self.tr("Opening rules map failed:\n%s"%tmxFormat.errorString()) + '\n' ret = False continue tilesetManager = TilesetManager.instance() tilesetManager.addReferences(rules.tilesets()) autoMapper = None autoMapper = AutoMapper(self.mMapDocument, rules, rulePath) self.mWarning += autoMapper.warningString() error = autoMapper.errorString() if error != '': self.mAutoMappers.append(autoMapper) else: self.mError += error del autoMapper if (rulePath.lower().endswith(".txt")): if (not self.loadFile(rulePath)): ret = False return ret ## # Applies automapping to the Region \a where, considering only layer # \a touchedLayer has changed. # There will only those Automappers be used which have a rule layer # touching the \a touchedLayer # If layer is 0, all Automappers are used. ## 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) ## # deletes all its data structures ## def cleanUp(self): self.mAutoMappers.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()
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 ObjectSelectionTool(AbstractObjectTool): def __init__(self, parent=None): super().__init__(self.tr("Select Objects"), QIcon(":images/22x22/tool-select-objects.png"), QKeySequence(self.tr("S")), parent) self.mSelectionRectangle = SelectionRectangle() self.mOriginIndicator = OriginIndicator() self.mMousePressed = False self.mHoveredObjectItem = None self.mClickedObjectItem = None self.mClickedRotateHandle = None self.mClickedResizeHandle = None self.mResizingLimitHorizontal = False self.mResizingLimitVertical = False self.mMode = Mode.Resize self.mAction = Action.NoAction self.mRotateHandles = [0, 0, 0, 0] self.mResizeHandles = [0, 0, 0, 0, 0, 0, 0, 0] self.mAlignPosition = QPointF() self.mMovingObjects = QVector() self.mScreenStart = QPoint() self.mStart = QPointF() self.mModifiers = 0 self.mOrigin = QPointF() for i in range(AnchorPosition.CornerAnchorCount): self.mRotateHandles[i] = RotateHandle(i) for i in range(AnchorPosition.AnchorCount): self.mResizeHandles[i] = ResizeHandle(i) def __del__(self): if self.mSelectionRectangle.scene(): self.mSelectionRectangle.scene().removeItem( self.mSelectionRectangle) if self.mOriginIndicator.scene(): self.mOriginIndicator.scene().removeItem(self.mOriginIndicator) for i in range(AnchorPosition.CornerAnchorCount): handle = self.mRotateHandles[i] scene = handle.scene() if scene: scene.removeItem(handle) self.mRotateHandles.clear() for i in range(AnchorPosition.AnchorCount): handle = self.mResizeHandles[i] scene = handle.scene() if scene: scene.removeItem(handle) self.mResizeHandles.clear() def tr(self, sourceText, disambiguation='', n=-1): return QCoreApplication.translate('ObjectSelectionTool', sourceText, disambiguation, n) def activate(self, scene): super().activate(scene) self.updateHandles() self.mapDocument().objectsChanged.connect(self.updateHandles) self.mapDocument().mapChanged.connect(self.updateHandles) scene.selectedObjectItemsChanged.connect(self.updateHandles) self.mapDocument().objectsRemoved.connect(self.objectsRemoved) if self.mOriginIndicator.scene() != scene: scene.addItem(self.mOriginIndicator) for i in range(AnchorPosition.CornerAnchorCount): handle = self.mRotateHandles[i] if handle.scene() != scene: scene.addItem(handle) for i in range(AnchorPosition.AnchorCount): handle = self.mResizeHandles[i] if handle.scene() != scene: scene.addItem(handle) def deactivate(self, scene): if self.mOriginIndicator.scene() == scene: scene.removeItem(self.mOriginIndicator) for i in range(AnchorPosition.CornerAnchorCount): handle = self.mRotateHandles[i] if handle.scene() == scene: scene.removeItem(handle) for i in range(AnchorPosition.AnchorCount): handle = self.mResizeHandles[i] if handle.scene() == scene: scene.removeItem(handle) self.mapDocument().objectsChanged.disconnect(self.updateHandles) self.mapDocument().mapChanged.disconnect(self.updateHandles) scene.selectedObjectItemsChanged.disconnect(self.updateHandles) super().deactivate(scene) def keyPressed(self, event): if (self.mAction != Action.NoAction): event.ignore() return moveBy = QPointF() x = event.key() if x == Qt.Key_Up: moveBy = QPointF(0, -1) elif x == Qt.Key_Down: moveBy = QPointF(0, 1) elif x == Qt.Key_Left: moveBy = QPointF(-1, 0) elif x == Qt.Key_Right: moveBy = QPointF(1, 0) else: super().keyPressed(event) return items = self.mapScene().selectedObjectItems() modifiers = event.modifiers() if (moveBy.isNull() or items.isEmpty() or (modifiers & Qt.ControlModifier)): event.ignore() return moveFast = modifiers & Qt.ShiftModifier snapToFineGrid = preferences.Preferences.instance().snapToFineGrid() if (moveFast): # TODO: This only makes sense for orthogonal maps moveBy.setX(moveBy.x() * self.mapDocument().map().tileWidth()) moveBy.setX(moveBy.y() * self.mapDocument().map().tileHeight()) if (snapToFineGrid): moveBy /= preferences.Preferences.instance().gridFine() undoStack = self.mapDocument().undoStack() undoStack.beginMacro(self.tr("Move %n Object(s)", "", items.size())) i = 0 for objectItem in items: object = objectItem.mapObject() oldPos = object.position() newPos = oldPos + moveBy undoStack.push( MoveMapObject(self.mapDocument(), object, newPos, oldPos)) i += 1 undoStack.endMacro() def mouseEntered(self): pass def mouseMoved(self, pos, modifiers): super().mouseMoved(pos, modifiers) # Update the hovered item (for mouse cursor) hoveredRotateHandle = None hoveredResizeHandle = None hoveredObjectItem = None view = self.mapScene().views()[0] if view: hoveredItem = self.mapScene().itemAt(pos, view.transform()) hoveredRotateHandle = None hoveredResizeHandle = None tp = type(hoveredItem) if tp == RotateHandle: hoveredRotateHandle = hoveredItem elif tp == ResizeHandle: hoveredResizeHandle = hoveredItem if (not hoveredRotateHandle and not hoveredResizeHandle): hoveredObjectItem = self.topMostObjectItemAt(pos) self.mHoveredObjectItem = hoveredObjectItem if (self.mAction == Action.NoAction and self.mMousePressed): screenPos = QCursor.pos() dragDistance = (self.mScreenStart - screenPos).manhattanLength() if (dragDistance >= QApplication.startDragDistance()): hasSelection = not self.mapScene().selectedObjectItems( ).isEmpty() # Holding Alt forces moving current selection # Holding Shift forces selection rectangle if ((self.mClickedObjectItem or (modifiers & Qt.AltModifier) and hasSelection) and not (modifiers & Qt.ShiftModifier)): self.startMoving(modifiers) elif (self.mClickedRotateHandle): self.startRotating() elif (self.mClickedResizeHandle): self.startResizing() else: self.startSelecting() x = self.mAction if x == Action.Selecting: self.mSelectionRectangle.setRectangle( QRectF(self.mStart, pos).normalized()) elif x == Action.Moving: self.updateMovingItems(pos, modifiers) elif x == Action.Rotating: self.updateRotatingItems(pos, modifiers) elif x == Action.Resizing: self.updateResizingItems(pos, modifiers) elif x == Action.NoAction: pass self.refreshCursor() def mousePressed(self, event): if (self.mAction != Action.NoAction ): # 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() clickedRotateHandle = 0 clickedResizeHandle = 0 view = findView(event) if view: clickedItem = self.mapScene().itemAt(event.scenePos(), view.transform()) clickedRotateHandle = None clickedResizeHandle = None tp = type(clickedItem) if tp == RotateHandle: clickedRotateHandle = clickedItem elif tp == ResizeHandle: clickedResizeHandle = clickedItem self.mClickedRotateHandle = clickedRotateHandle self.mClickedResizeHandle = clickedResizeHandle if (not clickedRotateHandle and not clickedResizeHandle): self.mClickedObjectItem = self.topMostObjectItemAt(self.mStart) else: super().mousePressed(event) 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 modifiersChanged(self, modifiers): self.mModifiers = modifiers self.refreshCursor() def languageChanged(self): self.setName(self.tr("Select Objects")) self.setShortcut(QKeySequence(self.tr("S"))) def updateHandles(self): if (self.mAction == Action.Moving or self.mAction == Action.Rotating or self.mAction == Action.Resizing): return objects = self.mapDocument().selectedObjects() showHandles = objects.size() > 0 if (showHandles): renderer = self.mapDocument().renderer() boundingRect = objectBounds( objects.first(), renderer, objectTransform(objects.first(), renderer)) for i in range(1, objects.size()): object = objects.at(i) boundingRect |= objectBounds(object, renderer, objectTransform(object, renderer)) topLeft = boundingRect.topLeft() topRight = boundingRect.topRight() bottomLeft = boundingRect.bottomLeft() bottomRight = boundingRect.bottomRight() center = boundingRect.center() handleRotation = 0 # If there is only one object selected, align to its orientation. if (objects.size() == 1): object = objects.first() handleRotation = object.rotation() if (resizeInPixelSpace(object)): bounds = pixelBounds(object) transform = QTransform(objectTransform(object, renderer)) topLeft = transform.map( renderer.pixelToScreenCoords_(bounds.topLeft())) topRight = transform.map( renderer.pixelToScreenCoords_(bounds.topRight())) bottomLeft = transform.map( renderer.pixelToScreenCoords_(bounds.bottomLeft())) bottomRight = transform.map( renderer.pixelToScreenCoords_(bounds.bottomRight())) center = transform.map( renderer.pixelToScreenCoords_(bounds.center())) # Ugly hack to make handles appear nicer in this case if (self.mapDocument().map().orientation() == Map.Orientation.Isometric): handleRotation += 45 else: bounds = objectBounds(object, renderer, QTransform()) transform = QTransform(objectTransform(object, renderer)) topLeft = transform.map(bounds.topLeft()) topRight = transform.map(bounds.topRight()) bottomLeft = transform.map(bounds.bottomLeft()) bottomRight = transform.map(bounds.bottomRight()) center = transform.map(bounds.center()) self.mOriginIndicator.setPos(center) self.mRotateHandles[AnchorPosition.TopLeftAnchor].setPos(topLeft) self.mRotateHandles[AnchorPosition.TopRightAnchor].setPos(topRight) self.mRotateHandles[AnchorPosition.BottomLeftAnchor].setPos( bottomLeft) self.mRotateHandles[AnchorPosition.BottomRightAnchor].setPos( bottomRight) top = (topLeft + topRight) / 2 left = (topLeft + bottomLeft) / 2 right = (topRight + bottomRight) / 2 bottom = (bottomLeft + bottomRight) / 2 self.mResizeHandles[AnchorPosition.TopAnchor].setPos(top) self.mResizeHandles[AnchorPosition.TopAnchor].setResizingOrigin( bottom) self.mResizeHandles[AnchorPosition.LeftAnchor].setPos(left) self.mResizeHandles[AnchorPosition.LeftAnchor].setResizingOrigin( right) self.mResizeHandles[AnchorPosition.RightAnchor].setPos(right) self.mResizeHandles[AnchorPosition.RightAnchor].setResizingOrigin( left) self.mResizeHandles[AnchorPosition.BottomAnchor].setPos(bottom) self.mResizeHandles[AnchorPosition.BottomAnchor].setResizingOrigin( top) self.mResizeHandles[AnchorPosition.TopLeftAnchor].setPos(topLeft) self.mResizeHandles[ AnchorPosition.TopLeftAnchor].setResizingOrigin(bottomRight) self.mResizeHandles[AnchorPosition.TopRightAnchor].setPos(topRight) self.mResizeHandles[ AnchorPosition.TopRightAnchor].setResizingOrigin(bottomLeft) self.mResizeHandles[AnchorPosition.BottomLeftAnchor].setPos( bottomLeft) self.mResizeHandles[ AnchorPosition.BottomLeftAnchor].setResizingOrigin(topRight) self.mResizeHandles[AnchorPosition.BottomRightAnchor].setPos( bottomRight) self.mResizeHandles[ AnchorPosition.BottomRightAnchor].setResizingOrigin(topLeft) for i in range(AnchorPosition.CornerAnchorCount): self.mRotateHandles[i].setRotation(handleRotation) for i in range(AnchorPosition.AnchorCount): self.mResizeHandles[i].setRotation(handleRotation) self.updateHandleVisibility() def updateHandleVisibility(self): hasSelection = not self.mapDocument().selectedObjects().isEmpty() showHandles = hasSelection and (self.mAction == Action.NoAction or self.mAction == Action.Selecting) showOrigin = hasSelection and self.mAction != Action.Moving and ( self.mMode == Mode.Rotate or self.mAction == Action.Resizing) for i in range(AnchorPosition.CornerAnchorCount): self.mRotateHandles[i].setVisible(showHandles and self.mMode == Mode.Rotate) for i in range(AnchorPosition.AnchorCount): self.mResizeHandles[i].setVisible(showHandles and self.mMode == Mode.Resize) self.mOriginIndicator.setVisible(showOrigin) def objectsRemoved(self, objects): if (self.mAction != Action.Moving and self.mAction != Action.Rotating and self.mAction != Action.Resizing): return # Abort move/rotate/resize to avoid crashing... # TODO: This should really not be allowed to happen in the first place. # since it breaks the undo history, for example. for i in range(self.mMovingObjects.size() - 1, -1, -1): object = self.mMovingObjects[i] mapObject = object.item.mapObject() if objects.contains(mapObject): # Avoid referencing the removed object self.mMovingObjects.remove(i) else: mapObject.setPosition(object.oldPosition) mapObject.setSize(object.oldSize) mapObject.setPolygon(object.oldPolygon) mapObject.setRotation(object.oldRotation) self.mapDocument().mapObjectModel().emitObjectsChanged( self.changingObjects) self.mMovingObjects.clear() 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 startSelecting(self): self.mAction = Action.Selecting self.mapScene().addItem(self.mSelectionRectangle) 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 updateMovingItems(self, pos, modifiers): renderer = self.mapDocument().renderer() diff = self.snapToGrid(pos - self.mStart, modifiers) for object in self.mMovingObjects: newPixelPos = object.oldItemPosition + diff newPos = renderer.screenToPixelCoords_(newPixelPos) mapObject = object.item.mapObject() mapObject.setPosition(newPos) self.mapDocument().mapObjectModel().emitObjectsChanged( self.changingObjects()) def finishMoving(self, pos): self.mAction = Action.NoAction self.updateHandles() if (self.mStart == pos): # Move is a no-op return undoStack = self.mapDocument().undoStack() undoStack.beginMacro( self.tr("Move %n Object(s)", "", self.mMovingObjects.size())) for object in self.mMovingObjects: undoStack.push( MoveMapObject(self.mapDocument(), object.item.mapObject(), object.oldPosition)) undoStack.endMacro() self.mMovingObjects.clear() def startRotating(self): self.mAction = Action.Rotating self.mOrigin = self.mOriginIndicator.pos() self.saveSelectionState() self.updateHandleVisibility() def updateRotatingItems(self, pos, modifiers): renderer = self.mapDocument().renderer() startDiff = self.mOrigin - self.mStart currentDiff = self.mOrigin - pos startAngle = math.atan2(startDiff.y(), startDiff.x()) currentAngle = math.atan2(currentDiff.y(), currentDiff.x()) angleDiff = currentAngle - startAngle snap = 15 * M_PI / 180 # 15 degrees in radians if (modifiers & Qt.ControlModifier): angleDiff = math.floor((angleDiff + snap / 2) / snap) * snap for object in self.mMovingObjects: mapObject = object.item.mapObject() offset = mapObject.objectGroup().offset() oldRelPos = object.oldItemPosition + offset - self.mOrigin sn = math.sin(angleDiff) cs = math.cos(angleDiff) newRelPos = QPointF(oldRelPos.x() * cs - oldRelPos.y() * sn, oldRelPos.x() * sn + oldRelPos.y() * cs) newPixelPos = self.mOrigin + newRelPos - offset newPos = renderer.screenToPixelCoords_(newPixelPos) newRotation = object.oldRotation + angleDiff * 180 / M_PI mapObject.setPosition(newPos) mapObject.setRotation(newRotation) self.mapDocument().mapObjectModel().emitObjectsChanged( self.changingObjects()) def finishRotating(self, pos): self.mAction = Action.NoAction self.updateHandles() if (self.mStart == pos): # No rotation at all return undoStack = self.mapDocument().undoStack() undoStack.beginMacro( self.tr("Rotate %n Object(s)", "", self.mMovingObjects.size())) for object in self.mMovingObjects: mapObject = object.item.mapObject() undoStack.push( MoveMapObject(self.mapDocument(), mapObject, object.oldPosition)) undoStack.push( RotateMapObject(self.mapDocument(), mapObject, object.oldRotation)) undoStack.endMacro() self.mMovingObjects.clear() def startResizing(self): self.mAction = Action.Resizing self.mOrigin = self.mOriginIndicator.pos() self.mResizingLimitHorizontal = self.mClickedResizeHandle.resizingLimitHorizontal( ) self.mResizingLimitVertical = self.mClickedResizeHandle.resizingLimitVertical( ) self.mStart = self.mClickedResizeHandle.pos() self.saveSelectionState() self.updateHandleVisibility() def updateResizingItems(self, pos, modifiers): renderer = self.mapDocument().renderer() resizingOrigin = self.mClickedResizeHandle.resizingOrigin() if (modifiers & Qt.ShiftModifier): resizingOrigin = self.mOrigin self.mOriginIndicator.setPos(resizingOrigin) ## Alternative toggle snap modifier, since Control is taken by the preserve # aspect ratio option. ## snapHelper = SnapHelper(renderer) if (modifiers & Qt.AltModifier): snapHelper.toggleSnap() pixelPos = renderer.screenToPixelCoords_(pos) snapHelper.snap(pixelPos) snappedScreenPos = renderer.pixelToScreenCoords_(pixelPos) diff = snappedScreenPos - resizingOrigin startDiff = self.mStart - resizingOrigin if (self.mMovingObjects.size() == 1): ## For single items the resizing is performed in object space in order # to handle different scaling on X and Y axis as well as to improve # handling of 0-sized objects. ## self.updateResizingSingleItem(resizingOrigin, snappedScreenPos, modifiers) return ## Calculate the scaling factor. Minimum is 1% to protect against making # everything 0-sized and non-recoverable (it's still possibly to run into # problems by repeatedly scaling down to 1%, but that's asking for it) ## scale = 0.0 if (self.mResizingLimitHorizontal): scale = max(0.01, diff.y() / startDiff.y()) elif (self.mResizingLimitVertical): scale = max(0.01, diff.x() / startDiff.x()) else: scale = min(max(0.01, diff.x() / startDiff.x()), max(0.01, diff.y() / startDiff.y())) if not math.isfinite(scale): scale = 1 for object in self.mMovingObjects: mapObject = object.item.mapObject() offset = mapObject.objectGroup().offset() oldRelPos = object.oldItemPosition + offset - resizingOrigin scaledRelPos = QPointF(oldRelPos.x() * scale, oldRelPos.y() * scale) newScreenPos = resizingOrigin + scaledRelPos - offset newPos = renderer.screenToPixelCoords_(newScreenPos) origSize = object.oldSize newSize = QSizeF(origSize.width() * scale, origSize.height() * scale) if (mapObject.polygon().isEmpty() == False): # For polygons, we have to scale in object space. rotation = object.item.rotation() * M_PI / -180 sn = math.sin(rotation) cs = math.cos(rotation) oldPolygon = object.oldPolygon newPolygon = QPolygonF(oldPolygon.size()) for n in range(oldPolygon.size()): oldPoint = QPointF(oldPolygon[n]) rotPoint = QPointF(oldPoint.x() * cs + oldPoint.y() * sn, oldPoint.y() * cs - oldPoint.x() * sn) scaledPoint = QPointF(rotPoint.x() * scale, rotPoint.y() * scale) newPoint = QPointF( scaledPoint.x() * cs - scaledPoint.y() * sn, scaledPoint.y() * cs + scaledPoint.x() * sn) newPolygon[n] = newPoint mapObject.setPolygon(newPolygon) mapObject.setSize(newSize) mapObject.setPosition(newPos) self.mapDocument().mapObjectModel().emitObjectsChanged( self.changingObjects()) def updateResizingSingleItem(self, resizingOrigin, screenPos, modifiers): renderer = self.mapDocument().renderer() object = self.mMovingObjects.first() mapObject = object.item.mapObject() ## The resizingOrigin, screenPos and mStart are affected by the ObjectGroup # offset. We will un-apply it to these variables since the resize for # single items happens in local coordinate space. ## offset = mapObject.objectGroup().offset() ## These transformations undo and redo the object rotation, which is always # applied in screen space. ## unrotate = rotateAt(object.oldItemPosition, -object.oldRotation) rotate = rotateAt(object.oldItemPosition, object.oldRotation) origin = (resizingOrigin - offset) * unrotate pos = (screenPos - offset) * unrotate start = (self.mStart - offset) * unrotate oldPos = object.oldItemPosition ## In order for the resizing to work somewhat sanely in isometric mode, # the resizing is performed in pixel space except for tile objects, which # are not affected by isometric projection apart from their position. ## pixelSpace = resizeInPixelSpace(mapObject) preserveAspect = modifiers & Qt.ControlModifier if (pixelSpace): origin = renderer.screenToPixelCoords_(origin) pos = renderer.screenToPixelCoords_(pos) start = renderer.screenToPixelCoords_(start) oldPos = object.oldPosition newPos = oldPos newSize = object.oldSize ## In case one of the anchors was used as-is, the desired size can be # derived directly from the distance from the origin for rectangle # and ellipse objects. This allows scaling up a 0-sized object without # dealing with infinite scaling factor issues. # # For obvious reasons this can't work on polygons or polylines, nor when # preserving the aspect ratio. ## if (self.mClickedResizeHandle.resizingOrigin() == resizingOrigin and (mapObject.shape() == MapObject.Rectangle or mapObject.shape() == MapObject.Ellipse) and not preserveAspect): newBounds = QRectF(newPos, newSize) newBounds = align(newBounds, mapObject.alignment()) x = self.mClickedResizeHandle.anchorPosition() if x == AnchorPosition.LeftAnchor or x == AnchorPosition.TopLeftAnchor or x == AnchorPosition.BottomLeftAnchor: newBounds.setLeft(min(pos.x(), origin.x())) elif x == AnchorPosition.RightAnchor or x == AnchorPosition.TopRightAnchor or x == AnchorPosition.BottomRightAnchor: newBounds.setRight(max(pos.x(), origin.x())) else: # nothing to do on this axis pass x = self.mClickedResizeHandle.anchorPosition() if x == AnchorPosition.TopAnchor or x == AnchorPosition.TopLeftAnchor or x == AnchorPosition.TopRightAnchor: newBounds.setTop(min(pos.y(), origin.y())) elif x == AnchorPosition.BottomAnchor or x == AnchorPosition.BottomLeftAnchor or x == AnchorPosition.BottomRightAnchor: newBounds.setBottom(max(pos.y(), origin.y())) else: # nothing to do on this axis pass newBounds = unalign(newBounds, mapObject.alignment()) newSize = newBounds.size() newPos = newBounds.topLeft() else: relPos = pos - origin startDiff = start - origin try: newx = relPos.x() / startDiff.x() except: newx = 0 try: newy = relPos.y() / startDiff.y() except: newy = 0 scalingFactor = QSizeF(max(0.01, newx), max(0.01, newy)) if not math.isfinite(scalingFactor.width()): scalingFactor.setWidth(1) if not math.isfinite(scalingFactor.height()): scalingFactor.setHeight(1) if (self.mResizingLimitHorizontal): if preserveAspect: scalingFactor.setWidth(scalingFactor.height()) else: scalingFactor.setWidth(1) elif (self.mResizingLimitVertical): if preserveAspect: scalingFactor.setHeight(scalingFactor.width()) else: scalingFactor.setHeight(1) elif (preserveAspect): scale = min(scalingFactor.width(), scalingFactor.height()) scalingFactor.setWidth(scale) scalingFactor.setHeight(scale) oldRelPos = oldPos - origin newPos = origin + QPointF(oldRelPos.x() * scalingFactor.width(), oldRelPos.y() * scalingFactor.height()) newSize.setWidth(newSize.width() * scalingFactor.width()) newSize.setHeight(newSize.height() * scalingFactor.height()) if (not object.oldPolygon.isEmpty()): newPolygon = QPolygonF(object.oldPolygon.size()) for n in range(object.oldPolygon.size()): point = object.oldPolygon[n] newPolygon[n] = QPointF(point.x() * scalingFactor.width(), point.y() * scalingFactor.height()) mapObject.setPolygon(newPolygon) if (pixelSpace): newPos = renderer.pixelToScreenCoords_(newPos) newPos = renderer.screenToPixelCoords_(newPos * rotate) mapObject.setSize(newSize) mapObject.setPosition(newPos) self.mapDocument().mapObjectModel().emitObjectsChanged( self.changingObjects()) def finishResizing(self, pos): self.mAction = Action.NoAction self.updateHandles() if (self.mStart == pos): # No scaling at all return undoStack = self.mapDocument().undoStack() undoStack.beginMacro( self.tr("Resize %n Object(s)", "", self.mMovingObjects.size())) for object in self.mMovingObjects: mapObject = object.item.mapObject() undoStack.push( MoveMapObject(self.mapDocument(), mapObject, object.oldPosition)) undoStack.push( ResizeMapObject(self.mapDocument(), mapObject, object.oldSize)) if (not object.oldPolygon.isEmpty()): undoStack.push( ChangePolygon(self.mapDocument(), mapObject, object.oldPolygon)) undoStack.endMacro() self.mMovingObjects.clear() def setMode(self, mode): if (self.mMode != mode): self.mMode = mode self.updateHandles() def saveSelectionState(self): self.mMovingObjects.clear() # Remember the initial state before moving, resizing or rotating for item in self.mapScene().selectedObjectItems(): mapObject = item.mapObject() object = MovingObject() object.item = item object.oldItemPosition = item.pos() object.oldPosition = mapObject.position() object.oldSize = mapObject.size() object.oldPolygon = mapObject.polygon() object.oldRotation = mapObject.rotation() self.mMovingObjects.append(object) def refreshCursor(self): cursorShape = Qt.ArrowCursor if self.mAction == Action.NoAction: hasSelection = not self.mapScene().selectedObjectItems().isEmpty() if ((self.mHoveredObjectItem or ((self.mModifiers & Qt.AltModifier) and hasSelection)) and not (self.mModifiers & Qt.ShiftModifier)): cursorShape = Qt.SizeAllCursor elif self.mAction == Action.Moving: cursorShape = Qt.SizeAllCursor if self.cursor.shape != cursorShape: self.setCursor(cursorShape) def snapToGrid(self, diff, modifiers): renderer = self.mapDocument().renderer() snapHelper = SnapHelper(renderer, modifiers) if (snapHelper.snaps()): alignScreenPos = renderer.pixelToScreenCoords_(self.mAlignPosition) newAlignScreenPos = alignScreenPos + diff newAlignPixelPos = renderer.screenToPixelCoords_(newAlignScreenPos) snapHelper.snap(newAlignPixelPos) return renderer.pixelToScreenCoords_( newAlignPixelPos) - alignScreenPos return diff def changingObjects(self): changingObjects = QList() for movingObject in self.mMovingObjects: changingObjects.append(movingObject.item.mapObject()) return changingObjects
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 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 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 AutomappingManager(QObject): ## # This signal is emited after automapping was done and an error occurred. ## errorsOccurred = pyqtSignal(bool) ## # This signal is emited after automapping was done and a warning occurred. ## warningsOccurred = pyqtSignal(bool) ## # Constructor. ## def __init__(self, parent=None): super().__init__(parent) ## # The current map document. ## self.mMapDocument = None ## # For each new file of rules a new AutoMapper is setup. In this vector we # can store all of the AutoMappers in order. ## self.mAutoMappers = QVector() ## # This tells you if the rules for the current map document were already # loaded. ## self.mLoaded = False ## # Contains all errors which occurred until canceling. # If mError is not empty, no serious result can be expected. ## self.mError = '' ## # Contains all strings, which try to explain unusual and unexpected # behavior. ## self.mWarning = QString() def __del__(self): self.cleanUp() def setMapDocument(self, mapDocument): self.cleanUp() if (self.mMapDocument): self.mMapDocument.disconnect() self.mMapDocument = mapDocument if (self.mMapDocument): self.mMapDocument.regionEdited.connect(self.autoMap) self.mLoaded = False def errorString(self): return self.mError def warningString(self): return self.mWarning ## # This triggers an automapping on the whole current map document. ## def autoMap(self, *args): l = len(args) if l == 0: if (not self.mMapDocument): return map = self.mMapDocument.Map() w = map.width() h = map.height() self.autoMapInternal(QRect(0, 0, w, h), None) elif l == 2: where, touchedLayer = args if (preferences.Preferences.instance().automappingDrawing()): self.autoMapInternal(where, touchedLayer) ## # This function parses a rules file. # For each path which is a rule, (fileextension is tmx) an AutoMapper # object is setup. # # If a fileextension is txt, this file will be opened and searched for # rules again. # # @return if the loading was successful: return True if it suceeded. ## def loadFile(self, filePath): ret = True absPath = QFileInfo(filePath).path() rulesFile = QFile(filePath) if (not rulesFile.exists()): self.mError += self.tr("No rules file found at:\n%s\n" % filePath) return False if (not rulesFile.open(QIODevice.ReadOnly | QIODevice.Text)): self.mError += self.tr("Error opening rules file:\n%s\n" % filePath) return False i = QTextStream(rulesFile) line = ' ' while line != '': line = i.readLine() rulePath = line.strip() if (rulePath == '' or rulePath.startswith('#') or rulePath.startswith("//")): continue if (QFileInfo(rulePath).isRelative()): rulePath = absPath + '/' + rulePath if (not QFileInfo(rulePath).exists()): self.mError += self.tr("File not found:\n%s" % rulePath) + '\n' ret = False continue if (rulePath.lower().endswith(".tmx")): tmxFormat = TmxMapFormat() rules = tmxFormat.read(rulePath) if (not rules): self.mError += self.tr("Opening rules map failed:\n%s" % tmxFormat.errorString()) + '\n' ret = False continue tilesetManager = TilesetManager.instance() tilesetManager.addReferences(rules.tilesets()) autoMapper = None autoMapper = AutoMapper(self.mMapDocument, rules, rulePath) self.mWarning += autoMapper.warningString() error = autoMapper.errorString() if error != '': self.mAutoMappers.append(autoMapper) else: self.mError += error del autoMapper if (rulePath.lower().endswith(".txt")): if (not self.loadFile(rulePath)): ret = False return ret ## # Applies automapping to the Region \a where, considering only layer # \a touchedLayer has changed. # There will only those Automappers be used which have a rule layer # touching the \a touchedLayer # If layer is 0, all Automappers are used. ## 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) ## # deletes all its data structures ## def cleanUp(self): self.mAutoMappers.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()
class ObjectSelectionTool(AbstractObjectTool): def __init__(self, parent = None): super().__init__(self.tr("Select Objects"), QIcon(":images/22x22/tool-select-objects.png"), QKeySequence(self.tr("S")), parent) self.mSelectionRectangle = SelectionRectangle() self.mOriginIndicator = OriginIndicator() self.mMousePressed = False self.mHoveredObjectItem = None self.mClickedObjectItem = None self.mClickedRotateHandle = None self.mClickedResizeHandle = None self.mResizingLimitHorizontal = False self.mResizingLimitVertical = False self.mMode = Mode.Resize self.mAction = Action.NoAction self.mRotateHandles = [0, 0, 0, 0] self.mResizeHandles = [0, 0, 0, 0, 0, 0, 0, 0] self.mAlignPosition = QPointF() self.mMovingObjects = QVector() self.mScreenStart = QPoint() self.mStart = QPointF() self.mModifiers = 0 self.mOrigin = QPointF() for i in range(AnchorPosition.CornerAnchorCount): self.mRotateHandles[i] = RotateHandle(i) for i in range(AnchorPosition.AnchorCount): self.mResizeHandles[i] = ResizeHandle(i) def __del__(self): if self.mSelectionRectangle.scene(): self.mSelectionRectangle.scene().removeItem(self.mSelectionRectangle) if self.mOriginIndicator.scene(): self.mOriginIndicator.scene().removeItem(self.mOriginIndicator) for i in range(AnchorPosition.CornerAnchorCount): handle = self.mRotateHandles[i] scene = handle.scene() if scene: scene.removeItem(handle) self.mRotateHandles.clear() for i in range(AnchorPosition.AnchorCount): handle = self.mResizeHandles[i] scene = handle.scene() if scene: scene.removeItem(handle) self.mResizeHandles.clear() def tr(self, sourceText, disambiguation = '', n = -1): return QCoreApplication.translate('ObjectSelectionTool', sourceText, disambiguation, n) def activate(self, scene): super().activate(scene) self.updateHandles() self.mapDocument().objectsChanged.connect(self.updateHandles) self.mapDocument().mapChanged.connect(self.updateHandles) scene.selectedObjectItemsChanged.connect(self.updateHandles) self.mapDocument().objectsRemoved.connect(self.objectsRemoved) if self.mOriginIndicator.scene() != scene: scene.addItem(self.mOriginIndicator) for i in range(AnchorPosition.CornerAnchorCount): handle = self.mRotateHandles[i] if handle.scene() != scene: scene.addItem(handle) for i in range(AnchorPosition.AnchorCount): handle = self.mResizeHandles[i] if handle.scene() != scene: scene.addItem(handle) def deactivate(self, scene): if self.mOriginIndicator.scene() == scene: scene.removeItem(self.mOriginIndicator) for i in range(AnchorPosition.CornerAnchorCount): handle = self.mRotateHandles[i] if handle.scene() == scene: scene.removeItem(handle) for i in range(AnchorPosition.AnchorCount): handle = self.mResizeHandles[i] if handle.scene() == scene: scene.removeItem(handle) self.mapDocument().objectsChanged.disconnect(self.updateHandles) self.mapDocument().mapChanged.disconnect(self.updateHandles) scene.selectedObjectItemsChanged.disconnect(self.updateHandles) super().deactivate(scene) def keyPressed(self, event): if (self.mAction != Action.NoAction): event.ignore() return moveBy = QPointF() x = event.key() if x==Qt.Key_Up: moveBy = QPointF(0, -1) elif x==Qt.Key_Down: moveBy = QPointF(0, 1) elif x==Qt.Key_Left: moveBy = QPointF(-1, 0) elif x==Qt.Key_Right: moveBy = QPointF(1, 0) else: super().keyPressed(event) return items = self.mapScene().selectedObjectItems() modifiers = event.modifiers() if (moveBy.isNull() or items.isEmpty() or (modifiers & Qt.ControlModifier)): event.ignore() return moveFast = modifiers & Qt.ShiftModifier snapToFineGrid = preferences.Preferences.instance().snapToFineGrid() if (moveFast): # TODO: This only makes sense for orthogonal maps moveBy.setX(moveBy.x() * self.mapDocument().map().tileWidth()) moveBy.setX(moveBy.y() * self.mapDocument().map().tileHeight()) if (snapToFineGrid): moveBy /= preferences.Preferences.instance().gridFine() undoStack = self.mapDocument().undoStack() undoStack.beginMacro(self.tr("Move %n Object(s)", "", items.size())) i = 0 for objectItem in items: object = objectItem.mapObject() oldPos = object.position() newPos = oldPos + moveBy undoStack.push(MoveMapObject(self.mapDocument(), object, newPos, oldPos)) i += 1 undoStack.endMacro() def mouseEntered(self): pass def mouseMoved(self, pos, modifiers): super().mouseMoved(pos, modifiers) # Update the hovered item (for mouse cursor) hoveredRotateHandle = None hoveredResizeHandle = None hoveredObjectItem = None view = self.mapScene().views()[0] if view: hoveredItem = self.mapScene().itemAt(pos,view.transform()) hoveredRotateHandle = None hoveredResizeHandle = None tp = type(hoveredItem) if tp==RotateHandle: hoveredRotateHandle = hoveredItem elif tp==ResizeHandle: hoveredResizeHandle = hoveredItem if (not hoveredRotateHandle and not hoveredResizeHandle): hoveredObjectItem = self.topMostObjectItemAt(pos) self.mHoveredObjectItem = hoveredObjectItem if (self.mAction == Action.NoAction and self.mMousePressed): screenPos = QCursor.pos() dragDistance = (self.mScreenStart - screenPos).manhattanLength() if (dragDistance >= QApplication.startDragDistance()): hasSelection = not self.mapScene().selectedObjectItems().isEmpty() # Holding Alt forces moving current selection # Holding Shift forces selection rectangle if ((self.mClickedObjectItem or (modifiers & Qt.AltModifier) and hasSelection) and not (modifiers & Qt.ShiftModifier)): self.startMoving(modifiers) elif (self.mClickedRotateHandle): self.startRotating() elif (self.mClickedResizeHandle): self.startResizing() else: self.startSelecting() x = self.mAction if x==Action.Selecting: self.mSelectionRectangle.setRectangle(QRectF(self.mStart, pos).normalized()) elif x==Action.Moving: self.updateMovingItems(pos, modifiers) elif x==Action.Rotating: self.updateRotatingItems(pos, modifiers) elif x==Action.Resizing: self.updateResizingItems(pos, modifiers) elif x==Action.NoAction: pass self.refreshCursor() def mousePressed(self, event): if (self.mAction != Action.NoAction): # 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() clickedRotateHandle = 0 clickedResizeHandle = 0 view = findView(event) if view: clickedItem = self.mapScene().itemAt(event.scenePos(), view.transform()) clickedRotateHandle = None clickedResizeHandle = None tp = type(clickedItem) if tp==RotateHandle: clickedRotateHandle = clickedItem elif tp==ResizeHandle: clickedResizeHandle = clickedItem self.mClickedRotateHandle = clickedRotateHandle self.mClickedResizeHandle = clickedResizeHandle if (not clickedRotateHandle and not clickedResizeHandle): self.mClickedObjectItem = self.topMostObjectItemAt(self.mStart) else: super().mousePressed(event) 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 modifiersChanged(self, modifiers): self.mModifiers = modifiers self.refreshCursor() def languageChanged(self): self.setName(self.tr("Select Objects")) self.setShortcut(QKeySequence(self.tr("S"))) def updateHandles(self): if (self.mAction == Action.Moving or self.mAction == Action.Rotating or self.mAction == Action.Resizing): return objects = self.mapDocument().selectedObjects() showHandles = objects.size() > 0 if (showHandles): renderer = self.mapDocument().renderer() boundingRect = objectBounds(objects.first(), renderer, objectTransform(objects.first(), renderer)) for i in range(1, objects.size()): object = objects.at(i) boundingRect |= objectBounds(object, renderer, objectTransform(object, renderer)) topLeft = boundingRect.topLeft() topRight = boundingRect.topRight() bottomLeft = boundingRect.bottomLeft() bottomRight = boundingRect.bottomRight() center = boundingRect.center() handleRotation = 0 # If there is only one object selected, align to its orientation. if (objects.size() == 1): object = objects.first() handleRotation = object.rotation() if (resizeInPixelSpace(object)): bounds = pixelBounds(object) transform = QTransform(objectTransform(object, renderer)) topLeft = transform.map(renderer.pixelToScreenCoords_(bounds.topLeft())) topRight = transform.map(renderer.pixelToScreenCoords_(bounds.topRight())) bottomLeft = transform.map(renderer.pixelToScreenCoords_(bounds.bottomLeft())) bottomRight = transform.map(renderer.pixelToScreenCoords_(bounds.bottomRight())) center = transform.map(renderer.pixelToScreenCoords_(bounds.center())) # Ugly hack to make handles appear nicer in this case if (self.mapDocument().map().orientation() == Map.Orientation.Isometric): handleRotation += 45 else: bounds = objectBounds(object, renderer, QTransform()) transform = QTransform(objectTransform(object, renderer)) topLeft = transform.map(bounds.topLeft()) topRight = transform.map(bounds.topRight()) bottomLeft = transform.map(bounds.bottomLeft()) bottomRight = transform.map(bounds.bottomRight()) center = transform.map(bounds.center()) self.mOriginIndicator.setPos(center) self.mRotateHandles[AnchorPosition.TopLeftAnchor].setPos(topLeft) self.mRotateHandles[AnchorPosition.TopRightAnchor].setPos(topRight) self.mRotateHandles[AnchorPosition.BottomLeftAnchor].setPos(bottomLeft) self.mRotateHandles[AnchorPosition.BottomRightAnchor].setPos(bottomRight) top = (topLeft + topRight) / 2 left = (topLeft + bottomLeft) / 2 right = (topRight + bottomRight) / 2 bottom = (bottomLeft + bottomRight) / 2 self.mResizeHandles[AnchorPosition.TopAnchor].setPos(top) self.mResizeHandles[AnchorPosition.TopAnchor].setResizingOrigin(bottom) self.mResizeHandles[AnchorPosition.LeftAnchor].setPos(left) self.mResizeHandles[AnchorPosition.LeftAnchor].setResizingOrigin(right) self.mResizeHandles[AnchorPosition.RightAnchor].setPos(right) self.mResizeHandles[AnchorPosition.RightAnchor].setResizingOrigin(left) self.mResizeHandles[AnchorPosition.BottomAnchor].setPos(bottom) self.mResizeHandles[AnchorPosition.BottomAnchor].setResizingOrigin(top) self.mResizeHandles[AnchorPosition.TopLeftAnchor].setPos(topLeft) self.mResizeHandles[AnchorPosition.TopLeftAnchor].setResizingOrigin(bottomRight) self.mResizeHandles[AnchorPosition.TopRightAnchor].setPos(topRight) self.mResizeHandles[AnchorPosition.TopRightAnchor].setResizingOrigin(bottomLeft) self.mResizeHandles[AnchorPosition.BottomLeftAnchor].setPos(bottomLeft) self.mResizeHandles[AnchorPosition.BottomLeftAnchor].setResizingOrigin(topRight) self.mResizeHandles[AnchorPosition.BottomRightAnchor].setPos(bottomRight) self.mResizeHandles[AnchorPosition.BottomRightAnchor].setResizingOrigin(topLeft) for i in range(AnchorPosition.CornerAnchorCount): self.mRotateHandles[i].setRotation(handleRotation) for i in range(AnchorPosition.AnchorCount): self.mResizeHandles[i].setRotation(handleRotation) self.updateHandleVisibility() def updateHandleVisibility(self): hasSelection = not self.mapDocument().selectedObjects().isEmpty() showHandles = hasSelection and (self.mAction == Action.NoAction or self.mAction == Action.Selecting) showOrigin = hasSelection and self.mAction != Action.Moving and (self.mMode == Mode.Rotate or self.mAction == Action.Resizing) for i in range(AnchorPosition.CornerAnchorCount): self.mRotateHandles[i].setVisible(showHandles and self.mMode == Mode.Rotate) for i in range(AnchorPosition.AnchorCount): self.mResizeHandles[i].setVisible(showHandles and self.mMode == Mode.Resize) self.mOriginIndicator.setVisible(showOrigin) def objectsRemoved(self, objects): if (self.mAction != Action.Moving and self.mAction != Action.Rotating and self.mAction != Action.Resizing): return # Abort move/rotate/resize to avoid crashing... # TODO: This should really not be allowed to happen in the first place. # since it breaks the undo history, for example. for i in range(self.mMovingObjects.size() - 1, -1, -1): object = self.mMovingObjects[i] mapObject = object.item.mapObject() if objects.contains(mapObject): # Avoid referencing the removed object self.mMovingObjects.remove(i) else: mapObject.setPosition(object.oldPosition) mapObject.setSize(object.oldSize) mapObject.setPolygon(object.oldPolygon) mapObject.setRotation(object.oldRotation) self.mapDocument().mapObjectModel().emitObjectsChanged(self.changingObjects) self.mMovingObjects.clear() 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 startSelecting(self): self.mAction = Action.Selecting self.mapScene().addItem(self.mSelectionRectangle) 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 updateMovingItems(self, pos, modifiers): renderer = self.mapDocument().renderer() diff = self.snapToGrid(pos-self.mStart, modifiers) for object in self.mMovingObjects: newPixelPos = object.oldItemPosition + diff newPos = renderer.screenToPixelCoords_(newPixelPos) mapObject = object.item.mapObject() mapObject.setPosition(newPos) self.mapDocument().mapObjectModel().emitObjectsChanged(self.changingObjects()) def finishMoving(self, pos): self.mAction = Action.NoAction self.updateHandles() if (self.mStart == pos): # Move is a no-op return undoStack = self.mapDocument().undoStack() undoStack.beginMacro(self.tr("Move %n Object(s)", "", self.mMovingObjects.size())) for object in self.mMovingObjects: undoStack.push(MoveMapObject(self.mapDocument(), object.item.mapObject(), object.oldPosition)) undoStack.endMacro() self.mMovingObjects.clear() def startRotating(self): self.mAction = Action.Rotating self.mOrigin = self.mOriginIndicator.pos() self.saveSelectionState() self.updateHandleVisibility() def updateRotatingItems(self, pos, modifiers): renderer = self.mapDocument().renderer() startDiff = self.mOrigin - self.mStart currentDiff = self.mOrigin - pos startAngle = math.atan2(startDiff.y(), startDiff.x()) currentAngle = math.atan2(currentDiff.y(), currentDiff.x()) angleDiff = currentAngle - startAngle snap = 15 * M_PI / 180 # 15 degrees in radians if (modifiers & Qt.ControlModifier): angleDiff = math.floor((angleDiff + snap / 2) / snap) * snap for object in self.mMovingObjects: mapObject = object.item.mapObject() offset = mapObject.objectGroup().offset() oldRelPos = object.oldItemPosition + offset - self.mOrigin sn = math.sin(angleDiff) cs = math.cos(angleDiff) newRelPos = QPointF(oldRelPos.x() * cs - oldRelPos.y() * sn, oldRelPos.x() * sn + oldRelPos.y() * cs) newPixelPos = self.mOrigin + newRelPos - offset newPos = renderer.screenToPixelCoords_(newPixelPos) newRotation = object.oldRotation + angleDiff * 180 / M_PI mapObject.setPosition(newPos) mapObject.setRotation(newRotation) self.mapDocument().mapObjectModel().emitObjectsChanged(self.changingObjects()) def finishRotating(self, pos): self.mAction = Action.NoAction self.updateHandles() if (self.mStart == pos): # No rotation at all return undoStack = self.mapDocument().undoStack() undoStack.beginMacro(self.tr("Rotate %n Object(s)", "", self.mMovingObjects.size())) for object in self.mMovingObjects: mapObject = object.item.mapObject() undoStack.push(MoveMapObject(self.mapDocument(), mapObject, object.oldPosition)) undoStack.push(RotateMapObject(self.mapDocument(), mapObject, object.oldRotation)) undoStack.endMacro() self.mMovingObjects.clear() def startResizing(self): self.mAction = Action.Resizing self.mOrigin = self.mOriginIndicator.pos() self.mResizingLimitHorizontal = self.mClickedResizeHandle.resizingLimitHorizontal() self.mResizingLimitVertical = self.mClickedResizeHandle.resizingLimitVertical() self.mStart = self.mClickedResizeHandle.pos() self.saveSelectionState() self.updateHandleVisibility() def updateResizingItems(self, pos, modifiers): renderer = self.mapDocument().renderer() resizingOrigin = self.mClickedResizeHandle.resizingOrigin() if (modifiers & Qt.ShiftModifier): resizingOrigin = self.mOrigin self.mOriginIndicator.setPos(resizingOrigin) ## Alternative toggle snap modifier, since Control is taken by the preserve # aspect ratio option. ## snapHelper = SnapHelper(renderer) if (modifiers & Qt.AltModifier): snapHelper.toggleSnap() pixelPos = renderer.screenToPixelCoords_(pos) snapHelper.snap(pixelPos) snappedScreenPos = renderer.pixelToScreenCoords_(pixelPos) diff = snappedScreenPos - resizingOrigin startDiff = self.mStart - resizingOrigin if (self.mMovingObjects.size() == 1): ## For single items the resizing is performed in object space in order # to handle different scaling on X and Y axis as well as to improve # handling of 0-sized objects. ## self.updateResizingSingleItem(resizingOrigin, snappedScreenPos, modifiers) return ## Calculate the scaling factor. Minimum is 1% to protect against making # everything 0-sized and non-recoverable (it's still possibly to run into # problems by repeatedly scaling down to 1%, but that's asking for it) ## scale = 0.0 if (self.mResizingLimitHorizontal): scale = max(0.01, diff.y() / startDiff.y()) elif (self.mResizingLimitVertical): scale = max(0.01, diff.x() / startDiff.x()) else: scale = min(max(0.01, diff.x() / startDiff.x()), max(0.01, diff.y() / startDiff.y())) if not math.isfinite(scale): scale = 1 for object in self.mMovingObjects: mapObject = object.item.mapObject() offset = mapObject.objectGroup().offset() oldRelPos = object.oldItemPosition + offset - resizingOrigin scaledRelPos = QPointF(oldRelPos.x() * scale, oldRelPos.y() * scale) newScreenPos = resizingOrigin + scaledRelPos - offset newPos = renderer.screenToPixelCoords_(newScreenPos) origSize = object.oldSize newSize = QSizeF(origSize.width() * scale, origSize.height() * scale) if (mapObject.polygon().isEmpty() == False): # For polygons, we have to scale in object space. rotation = object.item.rotation() * M_PI / -180 sn = math.sin(rotation) cs = math.cos(rotation) oldPolygon = object.oldPolygon newPolygon = QPolygonF(oldPolygon.size()) for n in range(oldPolygon.size()): oldPoint = QPointF(oldPolygon[n]) rotPoint = QPointF(oldPoint.x() * cs + oldPoint.y() * sn, oldPoint.y() * cs - oldPoint.x() * sn) scaledPoint = QPointF(rotPoint.x() * scale, rotPoint.y() * scale) newPoint = QPointF(scaledPoint.x() * cs - scaledPoint.y() * sn, scaledPoint.y() * cs + scaledPoint.x() * sn) newPolygon[n] = newPoint mapObject.setPolygon(newPolygon) mapObject.setSize(newSize) mapObject.setPosition(newPos) self.mapDocument().mapObjectModel().emitObjectsChanged(self.changingObjects()) def updateResizingSingleItem(self, resizingOrigin, screenPos, modifiers): renderer = self.mapDocument().renderer() object = self.mMovingObjects.first() mapObject = object.item.mapObject() ## The resizingOrigin, screenPos and mStart are affected by the ObjectGroup # offset. We will un-apply it to these variables since the resize for # single items happens in local coordinate space. ## offset = mapObject.objectGroup().offset() ## These transformations undo and redo the object rotation, which is always # applied in screen space. ## unrotate = rotateAt(object.oldItemPosition, -object.oldRotation) rotate = rotateAt(object.oldItemPosition, object.oldRotation) origin = (resizingOrigin - offset) * unrotate pos = (screenPos - offset) * unrotate start = (self.mStart - offset) * unrotate oldPos = object.oldItemPosition ## In order for the resizing to work somewhat sanely in isometric mode, # the resizing is performed in pixel space except for tile objects, which # are not affected by isometric projection apart from their position. ## pixelSpace = resizeInPixelSpace(mapObject) preserveAspect = modifiers & Qt.ControlModifier if (pixelSpace): origin = renderer.screenToPixelCoords_(origin) pos = renderer.screenToPixelCoords_(pos) start = renderer.screenToPixelCoords_(start) oldPos = object.oldPosition newPos = oldPos newSize = object.oldSize ## In case one of the anchors was used as-is, the desired size can be # derived directly from the distance from the origin for rectangle # and ellipse objects. This allows scaling up a 0-sized object without # dealing with infinite scaling factor issues. # # For obvious reasons this can't work on polygons or polylines, nor when # preserving the aspect ratio. ## if (self.mClickedResizeHandle.resizingOrigin() == resizingOrigin and (mapObject.shape() == MapObject.Rectangle or mapObject.shape() == MapObject.Ellipse) and not preserveAspect): newBounds = QRectF(newPos, newSize) newBounds = align(newBounds, mapObject.alignment()) x = self.mClickedResizeHandle.anchorPosition() if x==AnchorPosition.LeftAnchor or x==AnchorPosition.TopLeftAnchor or x==AnchorPosition.BottomLeftAnchor: newBounds.setLeft(min(pos.x(), origin.x())) elif x==AnchorPosition.RightAnchor or x==AnchorPosition.TopRightAnchor or x==AnchorPosition.BottomRightAnchor: newBounds.setRight(max(pos.x(), origin.x())) else: # nothing to do on this axis pass x = self.mClickedResizeHandle.anchorPosition() if x==AnchorPosition.TopAnchor or x==AnchorPosition.TopLeftAnchor or x==AnchorPosition.TopRightAnchor: newBounds.setTop(min(pos.y(), origin.y())) elif x==AnchorPosition.BottomAnchor or x==AnchorPosition.BottomLeftAnchor or x==AnchorPosition.BottomRightAnchor: newBounds.setBottom(max(pos.y(), origin.y())) else: # nothing to do on this axis pass newBounds = unalign(newBounds, mapObject.alignment()) newSize = newBounds.size() newPos = newBounds.topLeft() else: relPos = pos - origin startDiff = start - origin try: newx = relPos.x() / startDiff.x() except: newx = 0 try: newy = relPos.y() / startDiff.y() except: newy = 0 scalingFactor = QSizeF(max(0.01, newx), max(0.01, newy)) if not math.isfinite(scalingFactor.width()): scalingFactor.setWidth(1) if not math.isfinite(scalingFactor.height()): scalingFactor.setHeight(1) if (self.mResizingLimitHorizontal): if preserveAspect: scalingFactor.setWidth(scalingFactor.height()) else: scalingFactor.setWidth(1) elif (self.mResizingLimitVertical): if preserveAspect: scalingFactor.setHeight(scalingFactor.width()) else: scalingFactor.setHeight(1) elif (preserveAspect): scale = min(scalingFactor.width(), scalingFactor.height()) scalingFactor.setWidth(scale) scalingFactor.setHeight(scale) oldRelPos = oldPos - origin newPos = origin + QPointF(oldRelPos.x() * scalingFactor.width(), oldRelPos.y() * scalingFactor.height()) newSize.setWidth(newSize.width() * scalingFactor.width()) newSize.setHeight(newSize.height() * scalingFactor.height()) if (not object.oldPolygon.isEmpty()): newPolygon = QPolygonF(object.oldPolygon.size()) for n in range(object.oldPolygon.size()): point = object.oldPolygon[n] newPolygon[n] = QPointF(point.x() * scalingFactor.width(), point.y() * scalingFactor.height()) mapObject.setPolygon(newPolygon) if (pixelSpace): newPos = renderer.pixelToScreenCoords_(newPos) newPos = renderer.screenToPixelCoords_(newPos * rotate) mapObject.setSize(newSize) mapObject.setPosition(newPos) self.mapDocument().mapObjectModel().emitObjectsChanged(self.changingObjects()) def finishResizing(self, pos): self.mAction = Action.NoAction self.updateHandles() if (self.mStart == pos): # No scaling at all return undoStack = self.mapDocument().undoStack() undoStack.beginMacro(self.tr("Resize %n Object(s)", "", self.mMovingObjects.size())) for object in self.mMovingObjects: mapObject = object.item.mapObject() undoStack.push(MoveMapObject(self.mapDocument(), mapObject, object.oldPosition)) undoStack.push(ResizeMapObject(self.mapDocument(), mapObject, object.oldSize)) if (not object.oldPolygon.isEmpty()): undoStack.push(ChangePolygon(self.mapDocument(), mapObject, object.oldPolygon)) undoStack.endMacro() self.mMovingObjects.clear() def setMode(self, mode): if (self.mMode != mode): self.mMode = mode self.updateHandles() def saveSelectionState(self): self.mMovingObjects.clear() # Remember the initial state before moving, resizing or rotating for item in self.mapScene().selectedObjectItems(): mapObject = item.mapObject() object = MovingObject() object.item = item object.oldItemPosition = item.pos() object.oldPosition = mapObject.position() object.oldSize = mapObject.size() object.oldPolygon = mapObject.polygon() object.oldRotation = mapObject.rotation() self.mMovingObjects.append(object) def refreshCursor(self): cursorShape = Qt.ArrowCursor if self.mAction == Action.NoAction: hasSelection = not self.mapScene().selectedObjectItems().isEmpty() if ((self.mHoveredObjectItem or ((self.mModifiers & Qt.AltModifier) and hasSelection)) and not (self.mModifiers & Qt.ShiftModifier)): cursorShape = Qt.SizeAllCursor elif self.mAction == Action.Moving: cursorShape = Qt.SizeAllCursor if self.cursor.shape != cursorShape: self.setCursor(cursorShape) def snapToGrid(self, diff, modifiers): renderer = self.mapDocument().renderer() snapHelper = SnapHelper(renderer, modifiers) if (snapHelper.snaps()): alignScreenPos = renderer.pixelToScreenCoords_(self.mAlignPosition) newAlignScreenPos = alignScreenPos + diff newAlignPixelPos = renderer.screenToPixelCoords_(newAlignScreenPos) snapHelper.snap(newAlignPixelPos) return renderer.pixelToScreenCoords_(newAlignPixelPos) - alignScreenPos return diff def changingObjects(self): changingObjects = QList() for movingObject in self.mMovingObjects: changingObjects.append(movingObject.item.mapObject()) return changingObjects
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 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)