def drawGrid(self, painter, rect, gridColor): tileWidth = self.map().tileWidth() tileHeight = self.map().tileHeight() r = rect.toAlignedRect() r.adjust(-tileWidth / 2, -tileHeight / 2, tileWidth / 2, tileHeight / 2) startX = int(max(0.0, self.screenToTileCoords_(r.topLeft()).x())) startY = int(max(0.0, self.screenToTileCoords_(r.topRight()).y())) endX = int(min(self.map().width(), self.screenToTileCoords_(r.bottomRight()).x())) endY = int(min(self.map().height(), self.screenToTileCoords_(r.bottomLeft()).y())) gridColor.setAlpha(128) gridPen = QPen(gridColor) gridPen.setCosmetic(True) _x = QVector() _x.append(2) _x.append(2) gridPen.setDashPattern(_x) painter.setPen(gridPen) for y in range(startY, endY+1): start = self.tileToScreenCoords(startX, y) end = self.tileToScreenCoords(endX, y) painter.drawLine(start, end) for x in range(startX, endX+1): start = self.tileToScreenCoords(x, startY) end = self.tileToScreenCoords(x, endY) painter.drawLine(start, end)
def readObjectTypes(self, fileName): self.mError = '' objectTypes = QVector() file = QFile(fileName) if (not file.open(QIODevice.ReadOnly | QIODevice.Text)): self.mError = QCoreApplication.translate("ObjectTypes", "Could not open file.") return objectTypes reader = QXmlStreamReader(file) if (not reader.readNextStartElement() or reader.name() != "objecttypes"): self.mError = QCoreApplication.translate( "ObjectTypes", "File doesn't contain object types.") return objectTypes while (reader.readNextStartElement()): if (reader.name() == "objecttype"): atts = reader.attributes() name = QString(atts.value("name")) color = QColor(atts.value("color")) objectTypes.append(ObjectType(name, color)) reader.skipCurrentElement() if (reader.hasError()): self.mError = QCoreApplication.translate( "ObjectTypes", "%s\n\nLine %d, column %d" % (reader.errorString(), reader.lineNumber(), reader.columnNumber())) return objectTypes return objectTypes
def drawGrid(self, painter, rect, gridColor): tileWidth = self.map().tileWidth() tileHeight = self.map().tileHeight() r = rect.toAlignedRect() r.adjust(-tileWidth / 2, -tileHeight / 2, tileWidth / 2, tileHeight / 2) startX = int(max(0.0, self.screenToTileCoords_(r.topLeft()).x())) startY = int(max(0.0, self.screenToTileCoords_(r.topRight()).y())) endX = int( min(self.map().width(), self.screenToTileCoords_(r.bottomRight()).x())) endY = int( min(self.map().height(), self.screenToTileCoords_(r.bottomLeft()).y())) gridColor.setAlpha(128) gridPen = QPen(gridColor) gridPen.setCosmetic(True) _x = QVector() _x.append(2) _x.append(2) gridPen.setDashPattern(_x) painter.setPen(gridPen) for y in range(startY, endY + 1): start = self.tileToScreenCoords(startX, y) end = self.tileToScreenCoords(endX, y) painter.drawLine(start, end) for x in range(startX, endX + 1): start = self.tileToScreenCoords(x, startY) end = self.tileToScreenCoords(x, endY) painter.drawLine(start, end)
def offsetTiles(self, offset, bounds, wrapX, wrapY): newGrid = QVector() for i in range(self.mWidth * self.mHeight): newGrid.append(Cell()) for y in range(self.mHeight): for x in range(self.mWidth): # Skip out of bounds tiles if (not bounds.contains(x, y)): newGrid[x + y * self.mWidth] = self.cellAt(x, y) continue # Get position to pull tile value from oldX = x - offset.x() oldY = y - offset.y() # Wrap x value that will be pulled from if (wrapX and bounds.width() > 0): while oldX < bounds.left(): oldX += bounds.width() while oldX > bounds.right(): oldX -= bounds.width() # Wrap y value that will be pulled from if (wrapY and bounds.height() > 0): while oldY < bounds.top(): oldY += bounds.height() while oldY > bounds.bottom(): oldY -= bounds.height() # Set the new tile if (self.contains(oldX, oldY) and bounds.contains(oldX, oldY)): newGrid[x + y * self.mWidth] = self.cellAt(oldX, oldY) else: newGrid[x + y * self.mWidth] = Cell() self.mGrid = newGrid
def drawGrid(self, painter, rect, gridColor): tileWidth = self.map().tileWidth() tileHeight = self.map().tileHeight() if (tileWidth <= 0 or tileHeight <= 0): return startX = max(0, int(rect.x() / tileWidth) * tileWidth) startY = max(0, int(rect.y() / tileHeight) * tileHeight) endX = min(math.ceil(rect.right()), self.map().width() * tileWidth + 1) endY = min(math.ceil(rect.bottom()), self.map().height() * tileHeight + 1) gridColor.setAlpha(128) gridPen = QPen(gridColor) gridPen.setCosmetic(True) _x = QVector() _x.append(2) _x.append(2) gridPen.setDashPattern(_x) if (startY < endY): gridPen.setDashOffset(startY) painter.setPen(gridPen) for x in range(startX, endX, tileWidth): painter.drawLine(x, startY, x, endY - 1) if (startX < endX): gridPen.setDashOffset(startX) painter.setPen(gridPen) for y in range(startY, endY, tileHeight): painter.drawLine(startX, y, endX - 1, y)
def readObjectTypes(self, fileName): self.mError = '' objectTypes = QVector() file = QFile(fileName) if (not file.open(QIODevice.ReadOnly | QIODevice.Text)): self.mError = QCoreApplication.translate( "ObjectTypes", "Could not open file.") return objectTypes reader = QXmlStreamReader(file) if (not reader.readNextStartElement() or reader.name() != "objecttypes"): self.mError = QCoreApplication.translate( "ObjectTypes", "File doesn't contain object types.") return objectTypes while (reader.readNextStartElement()): if (reader.name() == "objecttype"): atts = reader.attributes() name = QString(atts.value("name")) color = QColor(atts.value("color")) objectTypes.append(ObjectType(name, color)) reader.skipCurrentElement() if (reader.hasError()): self.mError = QCoreApplication.translate("ObjectTypes", "%s\n\nLine %d, column %d"%(reader.errorString(), reader.lineNumber(), reader.columnNumber())) return objectTypes return objectTypes
def removeObjectTypes(self, indexes): rows = QVector() for index in indexes: rows.append(index.row()) rows = sorted(rows) for i in range(len(rows) - 1, -1, -1): row = rows[i] self.beginRemoveRows(QModelIndex(), row, row) self.mObjectTypes.remove(row) self.endRemoveRows()
def cellsInRegion(list, r): cells = QVector() for tilelayer in list: for rect in r.rects(): for x in range(rect.left(), rect.right() + 1): for y in range(rect.top(), rect.bottom() + 1): cell = tilelayer.cellAt(x, y) if (not cells.contains(cell)): cells.append(cell) return cells
def cellsInRegion(list, r): cells = QVector() for tilelayer in list: for rect in r.rects(): for x in range(rect.left(), rect.right()+1): for y in range(rect.top(), rect.bottom()+1): cell = tilelayer.cellAt(x, y) if (not cells.contains(cell)): cells.append(cell) return cells
def __readAnimationFrames(self): frames = QVector() while (self.xml.readNextStartElement()): if (self.xml.name() == "frame"): atts = self.xml.attributes() frame = Frame() frame.tileId = Int(atts.value("tileid")) frame.duration = Int(atts.value("duration")) frames.append(frame) self.xml.skipCurrentElement() else: self.__readUnknownElement() return frames
def flip(self, direction): newGrid = QVector() for i in range(self.mWidth * self.mHeight): newGrid.append(Cell()) for y in range(self.mHeight): for x in range(self.mWidth): dest = newGrid[x + y * self.mWidth] if (direction == FlipDirection.FlipHorizontally): source = self.cellAt(self.mWidth - x - 1, y) dest = source dest.flippedHorizontally = not source.flippedHorizontally elif (direction == FlipDirection.FlipVertically): source = self.cellAt(x, self.mHeight - y - 1) dest = source dest.flippedVertically = not source.flippedVertically self.mGrid = newGrid
def resize(self, size, offset): if (self.size() == size and offset.isNull()): return newGrid = QVector() for i in range(size.width() * size.height()): newGrid.append(Cell()) # Copy over the preserved part startX = max(0, -offset.x()) startY = max(0, -offset.y()) endX = min(self.mWidth, size.width() - offset.x()) endY = min(self.mHeight, size.height() - offset.y()) for y in range(startY, endY): for x in range(startX, endX): index = x + offset.x() + (y + offset.y()) * size.width() newGrid[index] = self.cellAt(x, y) self.mGrid = newGrid self.setSize(size)
class SetProperty(QUndoCommand): ## # Constructs a new 'Set Property' command. # # @param mapDocument the map document of the object's map # @param objects the objects of which the property should be changed # @param name the name of the property to be changed # @param value the new value of the property ## def __init__(self, mapDocument, objects, name, value, parent=None): super().__init__(parent) self.mProperties = QVector() self.mMapDocument = mapDocument self.mObjects = objects self.mName = name self.mValue = value for obj in self.mObjects: prop = ObjectProperty() prop.existed = obj.hasProperty(self.mName) prop.previousValue = obj.property(self.mName) self.mProperties.append(prop) if (self.mObjects.size() > 1 or self.mObjects[0].hasProperty(self.mName)): self.setText( QCoreApplication.translate("Undo Commands", "Set Property")) else: self.setText( QCoreApplication.translate("Undo Commands", "Add Property")) def undo(self): for i in range(self.mObjects.size()): if (self.mProperties[i].existed): self.mMapDocument.setProperty( self.mObjects[i], self.mName, self.mProperties[i].previousValue) else: self.mMapDocument.removeProperty(self.mObjects[i], self.mName) def redo(self): for obj in self.mObjects: self.mMapDocument.setProperty(obj, self.mName, self.mValue)
def autoMapInternal(self, where, touchedLayer): self.mError = '' self.mWarning = '' if (not self.mMapDocument): return automatic = touchedLayer != None if (not self.mLoaded): mapPath = QFileInfo(self.mMapDocument.fileName()).path() rulesFileName = mapPath + "/rules.txt" if (self.loadFile(rulesFileName)): self.mLoaded = True else: self.errorsOccurred.emit(automatic) return passedAutoMappers = QVector() if (touchedLayer): for a in self.mAutoMappers: if (a.ruleLayerNameUsed(touchedLayer.name())): passedAutoMappers.append(a) else: passedAutoMappers = self.mAutoMappers if (not passedAutoMappers.isEmpty()): # use a pointer to the region, so each automapper can manipulate it and the # following automappers do see the impact region = QRegion(where) undoStack = self.mMapDocument.undoStack() undoStack.beginMacro(self.tr("Apply AutoMap rules")) aw = AutoMapperWrapper(self.mMapDocument, passedAutoMappers, region) undoStack.push(aw) undoStack.endMacro() for automapper in self.mAutoMappers: self.mWarning += automapper.warningString() self.mError += automapper.errorString() if self.mWarning != '': self.warningsOccurred.emit(automatic) if self.mError != '': self.errorsOccurred.emit(automatic)
def pointsOnLine(*args): l = len(args) if l==2: a, b = args return pointsOnLine(a.x(), a.y(), b.x(), b.y()) else: x0, y0, x1, y1 = args ret = QVector() steep = abs(y1 - y0) > abs(x1 - x0) if steep: x0, y0 = y0, x0 x1, y1 = y1, x1 reverse = x0 > x1 if reverse: x0, x1 = x1, x0 y0, y1 = y1, y0 deltax = x1 - x0 deltay = abs(y1 - y0) error= int(deltax / 2) ystep = 0 y = y0 if (y0 < y1): ystep = 1 else: ystep = -1 for x in range(x0, x1+1): if (steep): ret.append(QPoint(y, x)) else: ret.append(QPoint(x, y)) error = error - deltay if (error < 0): y = y + ystep error = error + deltax if reverse: ret.reverse() return ret
def dropMimeData(self, data, action, row, column, parent): if (action == Qt.IgnoreAction): return True if (column > 0): return False beginRow = 0 if (row != -1): beginRow = row elif parent.isValid(): beginRow = parent.row() else: beginRow = self.mFrames.size() newFrames = QVector() if (data.hasFormat(FRAMES_MIMETYPE)): encodedData = data.data(FRAMES_MIMETYPE) stream = QDataStream(encodedData, QIODevice.ReadOnly) while (not stream.atEnd()): frame = Frame() frame.tileId = stream.readInt() frame.duration = stream.readInt() newFrames.append(frame) elif (data.hasFormat(TILES_MIMETYPE)): encodedData = data.data(TILES_MIMETYPE) stream = QDataStream(encodedData, QIODevice.ReadOnly) while (not stream.atEnd()): frame = Frame() frame.tileId = stream.readInt() frame.duration = FrameListModel.DEFAULT_DURATION newFrames.append(frame) if (newFrames.isEmpty()): return False self.beginInsertRows(QModelIndex(), beginRow, beginRow + newFrames.size() - 1) self.mFrames.insert(beginRow, newFrames.size(), Frame()) for i in range(newFrames.size()): self.mFrames[i + beginRow] = newFrames[i] self.endInsertRows() return True
class SetProperty(QUndoCommand): ## # Constructs a new 'Set Property' command. # # @param mapDocument the map document of the object's map # @param objects the objects of which the property should be changed # @param name the name of the property to be changed # @param value the new value of the property ## def __init__(self, mapDocument, objects, name, value, parent = None): super().__init__(parent) self.mProperties = QVector() self.mMapDocument = mapDocument self.mObjects = objects self.mName = name self.mValue = value for obj in self.mObjects: prop = ObjectProperty() prop.existed = obj.hasProperty(self.mName) prop.previousValue = obj.property(self.mName) self.mProperties.append(prop) if (self.mObjects.size() > 1 or self.mObjects[0].hasProperty(self.mName)): self.setText(QCoreApplication.translate("Undo Commands", "Set Property")) else: self.setText(QCoreApplication.translate("Undo Commands", "Add Property")) def undo(self): for i in range(self.mObjects.size()): if (self.mProperties[i].existed): self.mMapDocument.setProperty(self.mObjects[i], self.mName, self.mProperties[i].previousValue) else: self.mMapDocument.removeProperty(self.mObjects[i], self.mName) def redo(self): for obj in self.mObjects: self.mMapDocument.setProperty(obj, self.mName, self.mValue)
def __readTilesetTile(self, tileset): atts = self.xml.attributes() id = Int(atts.value("id")) if (id < 0): self.xml.raiseError(self.tr("Invalid tile ID: %d" % id)) return hasImage = tileset.imageSource() != '' if (hasImage and id >= tileset.tileCount()): self.xml.raiseError( self.tr("Tile ID does not exist in tileset image: %d" % id)) return if (id > tileset.tileCount()): self.xml.raiseError( self.tr("Invalid (nonconsecutive) tile ID: %d" % id)) return # For tilesets without image source, consecutive tile IDs are allowed (for # tiles with individual images) if (id == tileset.tileCount()): tileset.addTile(QPixmap()) tile = tileset.tileAt(id) # Read tile quadrant terrain ids terrain = atts.value("terrain") if terrain != '': quadrants = terrain.split(",") if (len(quadrants) == 4): for i in range(4): if quadrants[i] == '': t = -1 else: t = Int(quadrants[i]) tile.setCornerTerrainId(i, t) # Read tile probability probability = atts.value("probability") if probability != '': tile.setProbability(Float(probability)) while (self.xml.readNextStartElement()): if (self.xml.name() == "properties"): tile.mergeProperties(self.__readProperties()) elif (self.xml.name() == "image"): source = self.xml.attributes().value("source") if source != '': source = self.p.resolveReference(source, self.mPath) tileset.setTileImage(id, QPixmap.fromImage(self.__readImage()), source) elif (self.xml.name() == "objectgroup"): tile.setObjectGroup(self.__readObjectGroup()) elif (self.xml.name() == "animation"): tile.setFrames(self.__readAnimationFrames()) else: self.__readUnknownElement() # Temporary code to support TMW-style animation frame properties if (not tile.isAnimated() and tile.hasProperty("animation-frame0")): frames = QVector() i = 0 while (i >= 0): frameName = "animation-frame" + str(i) delayName = "animation-delay" + str(i) if (tile.hasProperty(frameName) and tile.hasProperty(delayName)): frame = Frame() frame.tileId = tile.property(frameName) frame.duration = tile.property(delayName) * 10 frames.append(frame) else: break i += 1 tile.setFrames(frames)
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
def __toTileset(self, variant): variantMap = variant firstGid = variantMap.get("firstgid",0) # Handle external tilesets sourceVariant = variantMap.get("source", '') if sourceVariant != '': source = resolvePath(self.mMapDir, sourceVariant) tileset, error = readTileset(source) if not tileset: self.mError = self.tr("Error while loading tileset '%s': %s"%(source, error)) else: self.mGidMapper.insert(firstGid, tileset) return tileset name = variantMap.get("name",'') tileWidth = variantMap.get("tilewidth",0) tileHeight = variantMap.get("tileheight",0) spacing = variantMap.get("spacing",0) margin = variantMap.get("margin",0) tileOffset = variantMap.get("tileoffset", {}) tileOffsetX = tileOffset.get("x",0) tileOffsetY = tileOffset.get("y",0) if (tileWidth <= 0 or tileHeight <= 0 or (firstGid == 0 and not self.mReadingExternalTileset)): self.mError = self.tr("Invalid tileset parameters for tileset '%s'"%name) return None tileset = Tileset.create(name, tileWidth, tileHeight, spacing, margin) tileset.setTileOffset(QPoint(tileOffsetX, tileOffsetY)) trans = variantMap.get("transparentcolor", '') if (trans!='' and QColor.isValidColor(trans)): tileset.setTransparentColor(QColor(trans)) imageVariant = variantMap.get("image",'') if imageVariant != '': imagePath = resolvePath(self.mMapDir, imageVariant) if (not tileset.loadFromImage(imagePath)): self.mError = self.tr("Error loading tileset image:\n'%s'"%imagePath) return None tileset.setProperties(self.toProperties(variantMap.get("properties", {}))) # Read terrains terrainsVariantList = variantMap.get("terrains", []) for terrainMap in terrainsVariantList: tileset.addTerrain(terrainMap.get("name", ''), terrainMap.get("tile", 0)) # Read tile terrain and external image information tilesVariantMap = variantMap.get("tiles", {}) for it in tilesVariantMap.items(): ok = False tileIndex = Int(it[0]) if (tileIndex < 0): self.mError = self.tr("Tileset tile index negative:\n'%d'"%tileIndex) if (tileIndex >= tileset.tileCount()): # Extend the tileset to fit the tile if (tileIndex >= len(tilesVariantMap)): # If tiles are defined this way, there should be an entry # for each tile. # Limit the index to number of entries to prevent running out # of memory on malicious input.f self.mError = self.tr("Tileset tile index too high:\n'%d'"%tileIndex) return None for i in range(tileset.tileCount(), tileIndex+1): tileset.addTile(QPixmap()) tile = tileset.tileAt(tileIndex) if (tile): tileVar = it[1] terrains = tileVar.get("terrain", []) if len(terrains) == 4: for i in range(0, 4): terrainId, ok = Int2(terrains[i]) if (ok and terrainId >= 0 and terrainId < tileset.terrainCount()): tile.setCornerTerrainId(i, terrainId) probability, ok = Float2(tileVar.get("probability", 0.0)) if (ok): tile.setProbability(probability) imageVariant = tileVar.get("image",'') if imageVariant != '': imagePath = resolvePath(self.mMapDir, imageVariant) tileset.setTileImage(tileIndex, QPixmap(imagePath), imagePath) objectGroupVariant = tileVar.get("objectgroup", {}) if len(objectGroupVariant) > 0: tile.setObjectGroup(self.toObjectGroup(objectGroupVariant)) frameList = tileVar.get("animation", []) lenFrames = len(frameList) if lenFrames > 0: frames = QVector() for i in range(lenFrames): frames.append(Frame()) for i in range(lenFrames - 1, -1, -1): frameVariantMap = frameList[i] frame = frames[i] frame.tileId = frameVariantMap.get("tileid", 0) frame.duration = frameVariantMap.get("duration", 0) tile.setFrames(frames) # Read tile properties propertiesVariantMap = variantMap.get("tileproperties", {}) for it in propertiesVariantMap.items(): tileIndex = Int(it[0]) propertiesVar = it[1] if (tileIndex >= 0 and tileIndex < tileset.tileCount()): properties = self.toProperties(propertiesVar) tileset.tileAt(tileIndex).setProperties(properties) if not self.mReadingExternalTileset: self.mGidMapper.insert(firstGid, tileset) return tileset
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
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)
class ObjectTypesModel(QAbstractTableModel): ColorRole = Qt.UserRole def __init__(self, parent): super().__init__(parent) self.mObjectTypes = QVector() def setObjectTypes(self, objectTypes): self.beginResetModel() self.mObjectTypes = objectTypes self.endResetModel() def objectTypes(self): return self.mObjectTypes def rowCount(self, parent): if parent.isValid(): _x = 0 else: _x = self.mObjectTypes.size() return _x def columnCount(self, parent): if parent.isValid(): _x = 0 else: _x = 2 return _x def headerData(self, section, orientation, role): if (orientation == Qt.Horizontal): if (role == Qt.DisplayRole): x = section if x == 0: return self.tr("Type") elif x == 1: return self.tr("Color") elif (role == Qt.TextAlignmentRole): return Qt.AlignLeft return QVariant() def data(self, index, role): # QComboBox requests data for an invalid index when the model is empty if (not index.isValid()): return QVariant() objectType = self.mObjectTypes.at(index.row()) if (role == Qt.DisplayRole or role == Qt.EditRole): if (index.column() == 0): return objectType.name if (role == ObjectTypesModel.ColorRole and index.column() == 1): return objectType.color return QVariant() def setData(self, index, value, role): if (role == Qt.EditRole and index.column() == 0): self.mObjectTypes[index.row()].name = value.strip() self.dataChanged.emit(index, index) return True return False def flags(self, index): f = super().flags(index) if (index.column() == 0): f |= Qt.ItemIsEditable return f def setObjectTypeColor(self, objectIndex, color): self.mObjectTypes[objectIndex].color = color mi = self.index(objectIndex, 1) self.dataChanged.emit(mi, mi) def removeObjectTypes(self, indexes): rows = QVector() for index in indexes: rows.append(index.row()) rows = sorted(rows) for i in range(len(rows) - 1, -1, -1): row = rows[i] self.beginRemoveRows(QModelIndex(), row, row) self.mObjectTypes.remove(row) self.endRemoveRows() def appendNewObjectType(self): self.beginInsertRows(QModelIndex(), self.mObjectTypes.size(), self.mObjectTypes.size()) self.mObjectTypes.append(ObjectType()) self.endInsertRows()
class TileStampManager(QObject): setStamp = pyqtSignal(TileStamp) def __init__(self, toolManager, parent=None): super().__init__(parent) self.mStampsByName = QMap() self.mQuickStamps = QVector() for i in range(TileStampManager.quickStampKeys().__len__()): self.mQuickStamps.append(0) self.mTileStampModel = TileStampModel(self) self.mToolManager = toolManager prefs = preferences.Preferences.instance() prefs.stampsDirectoryChanged.connect(self.stampsDirectoryChanged) self.mTileStampModel.stampAdded.connect(self.stampAdded) self.mTileStampModel.stampRenamed.connect(self.stampRenamed) self.mTileStampModel.stampChanged.connect(self.saveStamp) self.mTileStampModel.stampRemoved.connect(self.deleteStamp) self.loadStamps() def __del__(self): # needs to be over here where the TileStamp type is complete pass ## # Returns the keys used for quickly accessible tile stamps. # Note: To store a tile layer <Ctrl> is added. The given keys will work # for recalling the stored values. ## def quickStampKeys(): keys = [ Qt.Key_1, Qt.Key_2, Qt.Key_3, Qt.Key_4, Qt.Key_5, Qt.Key_6, Qt.Key_7, Qt.Key_8, Qt.Key_9 ] return keys def tileStampModel(self): return self.mTileStampModel def createStamp(self): stamp = self.tampFromContext(self.mToolManager.selectedTool()) if (not stamp.isEmpty()): self.mTileStampModel.addStamp(stamp) return stamp def addVariation(self, targetStamp): stamp = stampFromContext(self.mToolManager.selectedTool()) if (stamp.isEmpty()): return if (stamp == targetStamp): # avoid easy mistake of adding duplicates return for variation in stamp.variations(): self.mTileStampModel.addVariation(targetStamp, variation) def selectQuickStamp(self, index): stamp = self.mQuickStamps.at(index) if (not stamp.isEmpty()): self.setStamp.emit(stamp) def createQuickStamp(self, index): stamp = stampFromContext(self.mToolManager.selectedTool()) if (stamp.isEmpty()): return self.setQuickStamp(index, stamp) def extendQuickStamp(self, index): quickStamp = self.mQuickStamps[index] if (quickStamp.isEmpty()): self.createQuickStamp(index) else: self.addVariation(quickStamp) def stampsDirectoryChanged(self): # erase current stamps self.mQuickStamps.fill(TileStamp()) self.mStampsByName.clear() self.mTileStampModel.clear() self.loadStamps() def eraseQuickStamp(self, index): stamp = self.mQuickStamps.at(index) if (not stamp.isEmpty()): self.mQuickStamps[index] = TileStamp() if (not self.mQuickStamps.contains(stamp)): self.mTileStampModel.removeStamp(stamp) def setQuickStamp(self, index, stamp): stamp.setQuickStampIndex(index) # make sure existing quickstamp is removed from stamp model self.eraseQuickStamp(index) self.mTileStampModel.addStamp(stamp) self.mQuickStamps[index] = stamp def loadStamps(self): prefs = preferences.Preferences.instance() stampsDirectory = prefs.stampsDirectory() stampsDir = QDir(stampsDirectory) iterator = QDirIterator(stampsDirectory, ["*.stamp"], QDir.Files | QDir.Readable) while (iterator.hasNext()): stampFileName = iterator.next() stampFile = QFile(stampFileName) if (not stampFile.open(QIODevice.ReadOnly)): continue data = stampFile.readAll() document = QJsonDocument.fromBinaryData(data) if (document.isNull()): # document not valid binary data, maybe it's an JSON text file error = QJsonParseError() document = QJsonDocument.fromJson(data, error) if (error.error != QJsonParseError.NoError): qDebug("Failed to parse stamp file:" + error.errorString()) continue stamp = TileStamp.fromJson(document.object(), stampsDir) if (stamp.isEmpty()): continue stamp.setFileName(iterator.fileInfo().fileName()) self.mTileStampModel.addStamp(stamp) index = stamp.quickStampIndex() if (index >= 0 and index < self.mQuickStamps.size()): self.mQuickStamps[index] = stamp def stampAdded(self, stamp): if (stamp.name().isEmpty() or self.mStampsByName.contains(stamp.name())): # pick the first available stamp name name = QString() index = self.mTileStampModel.stamps().size() while (self.mStampsByName.contains(name)): name = str(index) index += 1 stamp.setName(name) self.mStampsByName.insert(stamp.name(), stamp) if (stamp.fileName().isEmpty()): stamp.setFileName(findStampFileName(stamp.name())) self.saveStamp(stamp) def stampRenamed(self, stamp): existingName = self.mStampsByName.key(stamp) self.mStampsByName.remove(existingName) self.mStampsByName.insert(stamp.name(), stamp) existingFileName = stamp.fileName() newFileName = findStampFileName(stamp.name(), existingFileName) if (existingFileName != newFileName): if (QFile.rename(stampFilePath(existingFileName), stampFilePath(newFileName))): stamp.setFileName(newFileName) def saveStamp(self, stamp): # make sure we have a stamps directory prefs = preferences.Preferences.instance() stampsDirectory = prefs.stampsDirectory() stampsDir = QDir(stampsDirectory) if (not stampsDir.exists() and not stampsDir.mkpath(".")): qDebug("Failed to create stamps directory" + stampsDirectory) return filePath = stampsDir.filePath(stamp.fileName()) file = QSaveFile(filePath) if (not file.open(QIODevice.WriteOnly)): qDebug("Failed to open stamp file for writing" + filePath) return stampJson = stamp.toJson(QFileInfo(filePath).dir()) file.write(QJsonDocument(stampJson).toJson(QJsonDocument.Compact)) if (not file.commit()): qDebug() << "Failed to write stamp" << filePath def deleteStamp(self, stamp): self.mStampsByName.remove(stamp.name()) QFile.remove(stampFilePath(stamp.fileName()))
def drawGrid(self, painter, exposed, gridColor): rect = exposed.toAlignedRect() if (rect.isNull()): return p = RenderParams(self.map()) # Determine the tile and pixel coordinates to start at startTile = self.screenToTileCoords_(rect.topLeft()).toPoint() startPos = self.tileToScreenCoords_(startTile).toPoint() ## Determine in which half of the tile the top-left corner of the area we # need to draw is. If we're in the upper half, we need to start one row # up due to those tiles being visible as well. How we go up one row # depends on whether we're in the left or right half of the tile. ## inUpperHalf = rect.y() - startPos.y() < p.sideOffsetY inLeftHalf = rect.x() - startPos.x() < p.sideOffsetX if (inUpperHalf): startTile.setY(startTile.y() - 1) if (inLeftHalf): startTile.setX(startTile.x() - 1) startTile.setX(max(0, startTile.x())) startTile.setY(max(0, startTile.y())) startPos = self.tileToScreenCoords_(startTile).toPoint() oct = [ QPoint(0, p.tileHeight - p.sideOffsetY), QPoint(0, p.sideOffsetY), QPoint(p.sideOffsetX, 0), QPoint(p.tileWidth - p.sideOffsetX, 0), QPoint(p.tileWidth, p.sideOffsetY), QPoint(p.tileWidth, p.tileHeight - p.sideOffsetY), QPoint(p.tileWidth - p.sideOffsetX, p.tileHeight), QPoint(p.sideOffsetX, p.tileHeight) ] lines = QVector() #lines.reserve(8) gridColor.setAlpha(128) gridPen = QPen(gridColor) gridPen.setCosmetic(True) _x = QVector() _x.append(2) _x.append(2) gridPen.setDashPattern(_x) painter.setPen(gridPen) if (p.staggerX): # Odd row shifting is applied in the rendering loop, so un-apply it here if (p.doStaggerX(startTile.x())): startPos.setY(startPos.y() - p.rowHeight) while (startPos.x() <= rect.right() and startTile.x() < self.map().width()): rowTile = QPoint(startTile) rowPos = QPoint(startPos) if (p.doStaggerX(startTile.x())): rowPos.setY(rowPos.y() + p.rowHeight) while (rowPos.y() <= rect.bottom() and rowTile.y() < self.map().height()): lines.append(QLineF(rowPos + oct[1], rowPos + oct[2])) lines.append(QLineF(rowPos + oct[2], rowPos + oct[3])) lines.append(QLineF(rowPos + oct[3], rowPos + oct[4])) isStaggered = p.doStaggerX(startTile.x()) lastRow = rowTile.y() == self.map().height() - 1 lastColumn = rowTile.x() == self.map().width() - 1 bottomLeft = rowTile.x() == 0 or (lastRow and isStaggered) bottomRight = lastColumn or (lastRow and isStaggered) if (bottomRight): lines.append(QLineF(rowPos + oct[5], rowPos + oct[6])) if (lastRow): lines.append(QLineF(rowPos + oct[6], rowPos + oct[7])) if (bottomLeft): lines.append(QLineF(rowPos + oct[7], rowPos + oct[0])) painter.drawLines(lines) lines.resize(0) rowPos.setY(rowPos.y() + p.tileHeight + p.sideLengthY) rowTile.setY(rowTile.y() + 1) startPos.setX(startPos.x() + p.columnWidth) startTile.setX(startTile.x() + 1) else: # Odd row shifting is applied in the rendering loop, so un-apply it here if (p.doStaggerY(startTile.y())): startPos.setX(startPos.x() - p.columnWidth) while (startPos.y() <= rect.bottom() and startTile.y() < self.map().height()): rowTile = QPoint(startTile) rowPos = QPoint(startPos) if (p.doStaggerY(startTile.y())): rowPos.setX(rowPos.x() + p.columnWidth) while (rowPos.x() <= rect.right() and rowTile.x() < self.map().width()): lines.append(QLineF(rowPos + oct[0], rowPos + oct[1])) lines.append(QLineF(rowPos + oct[1], rowPos + oct[2])) lines.append(QLineF(rowPos + oct[3], rowPos + oct[4])) isStaggered = p.doStaggerY(startTile.y()) lastRow = rowTile.y() == self.map().height() - 1 lastColumn = rowTile.x() == self.map().width() - 1 bottomLeft = lastRow or (rowTile.x() == 0 and not isStaggered) bottomRight = lastRow or (lastColumn and isStaggered) if (lastColumn): lines.append(QLineF(rowPos + oct[4], rowPos + oct[5])) if (bottomRight): lines.append(QLineF(rowPos + oct[5], rowPos + oct[6])) if (bottomLeft): lines.append(QLineF(rowPos + oct[7], rowPos + oct[0])) painter.drawLines(lines) lines.resize(0) rowPos.setX(rowPos.x() + p.tileWidth + p.sideLengthX) rowTile.setX(rowTile.x() + 1) startPos.setY(startPos.y() + p.rowHeight) startTile.setY(startTile.y() + 1)
class FrameListModel(QAbstractListModel): DEFAULT_DURATION = 100 def __init__(self, parent): super().__init__(parent) self.mFrames = QVector() self.mTileset = None def rowCount(self, parent): if parent.isValid(): _x = 0 else: _x = self.mFrames.size() return _x def data(self, index, role): x = role if x == Qt.EditRole or x == Qt.DisplayRole: return self.mFrames.at(index.row()).duration elif x == Qt.DecorationRole: tileId = self.mFrames.at(index.row()).tileId tile = self.mTileset.tileAt(tileId) if tile: return tile.image() return QVariant() def setData(self, index, value, role): if (role == Qt.EditRole): duration = value if (duration >= 0): self.mFrames[index.row()].duration = duration self.dataChanged.emit(index, index) return True return False def flags(self, index): defaultFlags = super().flags(index) if (index.isValid()): return Qt.ItemIsDragEnabled | Qt.ItemIsEditable | defaultFlags else: return Qt.ItemIsDropEnabled | defaultFlags def removeRows(self, row, count, parent): if (not parent.isValid()): if (count > 0): self.beginRemoveRows(parent, row, row + count - 1) self.mFrames.remove(row, count) self.endRemoveRows() return True return False def mimeTypes(self): types = QStringList() types.append(TILES_MIMETYPE) types.append(FRAMES_MIMETYPE) return types def mimeData(self, indexes): mimeData = QMimeData() encodedData = QByteArray() stream = QDataStream(encodedData, QIODevice.WriteOnly) for index in indexes: if (index.isValid()): frame = self.mFrames.at(index.row()) stream.writeInt(frame.tileId) stream.writeInt(frame.duration) mimeData.setData(FRAMES_MIMETYPE, encodedData) return mimeData def dropMimeData(self, data, action, row, column, parent): if (action == Qt.IgnoreAction): return True if (column > 0): return False beginRow = 0 if (row != -1): beginRow = row elif parent.isValid(): beginRow = parent.row() else: beginRow = self.mFrames.size() newFrames = QVector() if (data.hasFormat(FRAMES_MIMETYPE)): encodedData = data.data(FRAMES_MIMETYPE) stream = QDataStream(encodedData, QIODevice.ReadOnly) while (not stream.atEnd()): frame = Frame() frame.tileId = stream.readInt() frame.duration = stream.readInt() newFrames.append(frame) elif (data.hasFormat(TILES_MIMETYPE)): encodedData = data.data(TILES_MIMETYPE) stream = QDataStream(encodedData, QIODevice.ReadOnly) while (not stream.atEnd()): frame = Frame() frame.tileId = stream.readInt() frame.duration = FrameListModel.DEFAULT_DURATION newFrames.append(frame) if (newFrames.isEmpty()): return False self.beginInsertRows(QModelIndex(), beginRow, beginRow + newFrames.size() - 1) self.mFrames.insert(beginRow, newFrames.size(), Frame()) for i in range(newFrames.size()): self.mFrames[i + beginRow] = newFrames[i] self.endInsertRows() return True def supportedDropActions(self): return Qt.CopyAction | Qt.MoveAction def setFrames(self, tileset, frames): self.beginResetModel() self.mTileset = tileset self.mFrames = frames self.endResetModel() def addTileIdAsFrame(self, id): frame = Frame() frame.tileId = id frame.duration = FrameListModel.DEFAULT_DURATION self.addFrame(frame) def frames(self): return self.mFrames def addFrame(self, frame): self.beginInsertRows(QModelIndex(), self.mFrames.size(), self.mFrames.size()) self.mFrames.append(frame) self.endInsertRows()
class CellRenderer(): BottomLeft, BottomCenter, TopLeft = range(3) def __init__(self, painter): self.mPainter = painter self.mTile = None self.mIsOpenGL = hasOpenGLEngine(painter) self.mFragments = QVector() def __del__(self): self.flush() ## # Renders a \a cell with the given \a origin at \a pos, taking into account # the flipping and tile offset. # # For performance reasons, the actual drawing is delayed until a different # kind of tile has to be drawn. For this reason it is necessary to call # flush when finished doing drawCell calls. This function is also called by # the destructor so usually an explicit call is not needed. ## def render(self, cell, pos, cellSize, origin): if (self.mTile != cell.tile): self.flush() image = cell.tile.currentFrameImage() size = image.size() if cellSize == QSizeF(0,0): objectSize = size else: objectSize = cellSize scale = QSizeF(objectSize.width() / size.width(), objectSize.height() / size.height()) offset = cell.tile.offset() sizeHalf = QPointF(objectSize.width() / 2, objectSize.height() / 2) fragment = QPainter.PixmapFragment() fragment.x = pos.x() + (offset.x() * scale.width()) + sizeHalf.x() fragment.y = pos.y() + (offset.y() * scale.height()) + sizeHalf.y() - objectSize.height() fragment.sourceLeft = 0 fragment.sourceTop = 0 fragment.width = size.width() fragment.height = size.height() if cell.flippedHorizontally: fragment.scaleX = -1 else: fragment.scaleX = 1 if cell.flippedVertically: fragment.scaleY = -1 else: fragment.scaleY = 1 fragment.rotation = 0 fragment.opacity = 1 flippedHorizontally = cell.flippedHorizontally flippedVertically = cell.flippedVertically if (origin == CellRenderer.BottomCenter): fragment.x -= sizeHalf.x() if (cell.flippedAntiDiagonally): fragment.rotation = 90 flippedHorizontally = cell.flippedVertically flippedVertically = not cell.flippedHorizontally # Compensate for the swap of image dimensions halfDiff = sizeHalf.y() - sizeHalf.x() fragment.y += halfDiff if (origin != CellRenderer.BottomCenter): fragment.x += halfDiff if flippedHorizontally: x = -1 else: x = 1 fragment.scaleX = scale.width() * x if flippedVertically: x = -1 else: x = 1 fragment.scaleY = scale.height() * x if (self.mIsOpenGL or (fragment.scaleX > 0 and fragment.scaleY > 0)): self.mTile = cell.tile self.mFragments.append(fragment) return # The Raster paint engine as of Qt 4.8.4 / 5.0.2 does not support # drawing fragments with a negative scaling factor. self.flush() # make sure we drew all tiles so far oldTransform = self.mPainter.transform() transform = oldTransform transform.translate(fragment.x, fragment.y) transform.rotate(fragment.rotation) transform.scale(fragment.scaleX, fragment.scaleY) target = QRectF(fragment.width * -0.5, fragment.height * -0.5, fragment.width, fragment.height) source = QRectF(0, 0, fragment.width, fragment.height) self.mPainter.setTransform(transform) self.mPainter.drawPixmap(target, image, source) self.mPainter.setTransform(oldTransform) def flush(self): if (not self.mTile): return self.mPainter.drawPixmapFragments(self.mFragments, self.mTile.currentFrameImage()) self.mTile = None self.mFragments.resize(0)
class Preferences(QObject): showGridChanged = pyqtSignal(bool) showTileObjectOutlinesChanged = pyqtSignal(bool) showTileAnimationsChanged = pyqtSignal(bool) snapToGridChanged = pyqtSignal(bool) snapToFineGridChanged = pyqtSignal(bool) gridColorChanged = pyqtSignal(QColor) gridFineChanged = pyqtSignal(int) objectLineWidthChanged = pyqtSignal(float) highlightCurrentLayerChanged = pyqtSignal(bool) showTilesetGridChanged = pyqtSignal(bool) objectLabelVisibilityChanged = pyqtSignal(int) useOpenGLChanged = pyqtSignal(bool) objectTypesChanged = pyqtSignal() mapsDirectoryChanged = pyqtSignal() stampsDirectoryChanged = pyqtSignal(str) isPatronChanged = pyqtSignal() mInstance = None ObjectTypesFile, ImageFile, ExportedFile, ExternalTileset = range(4) def __init__(self): super().__init__() self.mSettings = QSettings(self) self.mObjectTypes = QVector() # Retrieve storage settings self.mSettings.beginGroup("Storage") self.mLayerDataFormat = Map.LayerDataFormat(self.intValue("LayerDataFormat", Map.LayerDataFormat.Base64Zlib.value)) self.mMapRenderOrder = Map.RenderOrder(self.intValue("MapRenderOrder", Map.RenderOrder.RightDown.value)) self.mDtdEnabled = self.boolValue("DtdEnabled") self.mReloadTilesetsOnChange = self.boolValue("ReloadTilesets", True) self.mStampsDirectory = self.stringValue("StampsDirectory") self.mSettings.endGroup() # Retrieve interface settings self.mSettings.beginGroup("Interface") self.mShowGrid = self.boolValue("ShowGrid") self.mShowTileObjectOutlines = self.boolValue("ShowTileObjectOutlines") self.mShowTileAnimations = self.boolValue("ShowTileAnimations", True) self.mSnapToGrid = self.boolValue("SnapToGrid") self.mSnapToFineGrid = self.boolValue("SnapToFineGrid") self.mGridColor = self.colorValue("GridColor", Qt.black) self.mGridFine = self.intValue("GridFine", 4) self.mObjectLineWidth = self.realValue("ObjectLineWidth", 2) self.mHighlightCurrentLayer = self.boolValue("HighlightCurrentLayer") self.mShowTilesetGrid = self.boolValue("ShowTilesetGrid", True) self.mLanguage = self.stringValue("Language") self.mUseOpenGL = self.boolValue("OpenGL") self.mObjectLabelVisibility = self.intValue("ObjectLabelVisibility", ObjectLabelVisiblity.AllObjectLabels) self.mSettings.endGroup() # Retrieve defined object types self.mSettings.beginGroup("ObjectTypes") names = self.mSettings.value("Names", QStringList()) colors = self.mSettings.value("Colors", QStringList()) self.mSettings.endGroup() count = min(len(names), len(colors)) for i in range(count): self.mObjectTypes.append(ObjectType(names[i], QColor(colors[i]))) self.mSettings.beginGroup("Automapping") self.mAutoMapDrawing = self.boolValue("WhileDrawing") self.mSettings.endGroup() self.mSettings.beginGroup("MapsDirectory") self.mMapsDirectory = self.stringValue("Current") self.mSettings.endGroup() tilesetManager = TilesetManager.instance() tilesetManager.setReloadTilesetsOnChange(self.mReloadTilesetsOnChange) tilesetManager.setAnimateTiles(self.mShowTileAnimations) # Keeping track of some usage information self.mSettings.beginGroup("Install") self.mFirstRun = QDate.fromString(self.mSettings.value("FirstRun")) self.mRunCount = self.intValue("RunCount", 0) + 1 self.mIsPatron = self.boolValue("IsPatron") if (not self.mFirstRun.isValid()): self.mFirstRun = QDate.currentDate() self.mSettings.setValue("FirstRun", self.mFirstRun.toString(Qt.ISODate)) self.mSettings.setValue("RunCount", self.mRunCount) self.mSettings.endGroup() # Retrieve startup settings self.mSettings.beginGroup("Startup") self.mOpenLastFilesOnStartup = self.boolValue("OpenLastFiles", True) self.mSettings.endGroup() def __del__(self): pass def setObjectLabelVisibility(self, visibility): if self.mObjectLabelVisibility == visibility: return self.mObjectLabelVisibility = visibility self.mSettings.setValue("Interface/ObjectLabelVisibility", visibility) self.objectLabelVisibilityChanged.emit(visibility) def instance(): if (not Preferences.mInstance): Preferences.mInstance = Preferences() return Preferences.mInstance def deleteInstance(): del Preferences.mInstance Preferences.mInstance = None def showGrid(self): return self.mShowGrid def showTileObjectOutlines(self): return self.mShowTileObjectOutlines def showTileAnimations(self): return self.mShowTileAnimations def snapToGrid(self): return self.mSnapToGrid def snapToFineGrid(self): return self.mSnapToFineGrid def gridColor(self): return self.mGridColor def gridFine(self): return self.mGridFine def objectLineWidth(self): return self.mObjectLineWidth def highlightCurrentLayer(self): return self.mHighlightCurrentLayer def showTilesetGrid(self): return self.mShowTilesetGrid def useOpenGL(self): return self.mUseOpenGL def objectTypes(self): return self.mObjectTypes def automappingDrawing(self): return self.mAutoMapDrawing ## # Provides access to the QSettings instance to allow storing/retrieving # arbitrary values. The naming style for groups and keys is CamelCase. ## def settings(self): return self.mSettings def layerDataFormat(self): return self.mLayerDataFormat def setLayerDataFormat(self, layerDataFormat): if (self.mLayerDataFormat == layerDataFormat): return self.mLayerDataFormat = layerDataFormat self.mSettings.setValue("Storage/LayerDataFormat", self.mLayerDataFormat) def mapRenderOrder(self): return self.mMapRenderOrder def setMapRenderOrder(self, mapRenderOrder): if (self.mMapRenderOrder == mapRenderOrder): return self.mMapRenderOrder = mapRenderOrder self.mSettings.setValue("Storage/MapRenderOrder", self.mMapRenderOrder) def dtdEnabled(self): return self.mDtdEnabled def setDtdEnabled(self, enabled): self.mDtdEnabled = enabled self.mSettings.setValue("Storage/DtdEnabled", enabled) def language(self): return self.mLanguage def setLanguage(self, language): if (self.mLanguage == language): return self.mLanguage = language self.mSettings.setValue("Interface/Language", self.mLanguage) languagemanager.LanguageManager.instance().installTranslators() def reloadTilesetsOnChange(self): return self.mReloadTilesetsOnChange def setReloadTilesetsOnChanged(self, value): if (self.mReloadTilesetsOnChange == value): return self.mReloadTilesetsOnChange = value self.mSettings.setValue("Storage/ReloadTilesets", self.mReloadTilesetsOnChange) tilesetManager = TilesetManager.instance() tilesetManager.setReloadTilesetsOnChange(self.mReloadTilesetsOnChange) def setUseOpenGL(self, useOpenGL): if (self.mUseOpenGL == useOpenGL): return self.mUseOpenGL = useOpenGL self.mSettings.setValue("Interface/OpenGL", self.mUseOpenGL) self.useOpenGLChanged.emit(self.mUseOpenGL) def setObjectTypes(self, objectTypes): self.mObjectTypes = objectTypes names = QStringList() colors = QStringList() for objectType in objectTypes: names.append(objectType.name) colors.append(objectType.color.name()) self.mSettings.beginGroup("ObjectTypes") self.mSettings.setValue("Names", names) self.mSettings.setValue("Colors", colors) self.mSettings.endGroup() self.objectTypesChanged.emit() def lastPath(self, fileType): path = self.mSettings.value(lastPathKey(fileType)) if path==None or path=='': documentManager = DocumentManager.instance() mapDocument = documentManager.currentDocument() if mapDocument: path = QFileInfo(mapDocument.fileName()).path() if path==None or path=='': path = QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation) return path ## # \see lastPath() ## def setLastPath(self, fileType, path): self.mSettings.setValue(lastPathKey(fileType), path) def setAutomappingDrawing(self, enabled): self.mAutoMapDrawing = enabled self.mSettings.setValue("Automapping/WhileDrawing", enabled) def mapsDirectory(self): return self.mMapsDirectory def setMapsDirectory(self, path): if (self.mMapsDirectory == path): return self.mMapsDirectory = path self.mSettings.setValue("MapsDirectory/Current", path) self.mapsDirectoryChanged.emit() def objectLabelVisibility(self): return self.mObjectLabelVisibility def firstRun(self): return self.mFirstRun def runCount(self): return self.mRunCount def isPatron(self): return self.mIsPatron def openLastFilesOnStartup(self): return self.mOpenLastFilesOnStartup def setPatron(self, isPatron): if (self.mIsPatron == isPatron): return self.mIsPatron = isPatron self.mSettings.setValue("Install/IsPatron", isPatron) self.isPatronChanged.emit() def setShowGrid(self, showGrid): if (self.mShowGrid == showGrid): return self.mShowGrid = showGrid self.mSettings.setValue("Interface/ShowGrid", self.mShowGrid) self.showGridChanged.emit(self.mShowGrid) def setShowTileObjectOutlines(self, enabled): if (self.mShowTileObjectOutlines == enabled): return self.mShowTileObjectOutlines = enabled self.mSettings.setValue("Interface/ShowTileObjectOutlines", self.mShowTileObjectOutlines) self.showTileObjectOutlinesChanged.emit(self.mShowTileObjectOutlines) def setShowTileAnimations(self, enabled): if (self.mShowTileAnimations == enabled): return self.mShowTileAnimations = enabled self.mSettings.setValue("Interface/ShowTileAnimations", self.mShowTileAnimations) tilesetManager = TilesetManager.instance() tilesetManager.setAnimateTiles(self.mShowTileAnimations) self.showTileAnimationsChanged.emit(self.mShowTileAnimations) def setSnapToGrid(self, snapToGrid): if (self.mSnapToGrid == snapToGrid): return self.mSnapToGrid = snapToGrid self.mSettings.setValue("Interface/SnapToGrid", self.mSnapToGrid) self.snapToGridChanged.emit(self.mSnapToGrid) def setSnapToFineGrid(self, snapToFineGrid): if (self.mSnapToFineGrid == snapToFineGrid): return self.mSnapToFineGrid = snapToFineGrid self.mSettings.setValue("Interface/SnapToFineGrid", self.mSnapToFineGrid) self.snapToFineGridChanged.emit(self.mSnapToFineGrid) def setGridColor(self, gridColor): if (self.mGridColor == gridColor): return self.mGridColor = gridColor self.mSettings.setValue("Interface/GridColor", self.mGridColor.name()) self.gridColorChanged.emit(self.mGridColor) def setGridFine(self, gridFine): if (self.mGridFine == gridFine): return self.mGridFine = gridFine self.mSettings.setValue("Interface/GridFine", self.mGridFine) self.gridFineChanged.emit(self.mGridFine) def setObjectLineWidth(self, lineWidth): if (self.mObjectLineWidth == lineWidth): return self.mObjectLineWidth = lineWidth self.mSettings.setValue("Interface/ObjectLineWidth", self.mObjectLineWidth) self.objectLineWidthChanged.emit(self.mObjectLineWidth) def setHighlightCurrentLayer(self, highlight): if (self.mHighlightCurrentLayer == highlight): return self.mHighlightCurrentLayer = highlight self.mSettings.setValue("Interface/HighlightCurrentLayer", self.mHighlightCurrentLayer) self.highlightCurrentLayerChanged.emit(self.mHighlightCurrentLayer) def setShowTilesetGrid(self, showTilesetGrid): if (self.mShowTilesetGrid == showTilesetGrid): return self.mShowTilesetGrid = showTilesetGrid self.mSettings.setValue("Interface/ShowTilesetGrid", self.mShowTilesetGrid) self.showTilesetGridChanged.emit(self.mShowTilesetGrid) def setOpenLastFilesOnStartup(self, open): if self.mOpenLastFilesOnStartup == open: return self.mOpenLastFilesOnStartup = open self.mSettings.setValue("Startup/OpenLastFiles", open) def boolValue(self, key, defaultValue = False): b = self.mSettings.value(key, defaultValue) tp = type(b) if tp==bool: return b elif tp==str: return b.lower()=='true' return bool(b) def colorValue(self, key, default = QColor()): if type(default) != QColor: default = QColor(default) name = self.mSettings.value(key, default.name()) if (not QColor.isValidColor(name)): return QColor() return QColor(name) def stringValue(self, key, default = ''): return self.mSettings.value(key, default) def intValue(self, key, defaultValue): return Int(self.mSettings.value(key, defaultValue)) def realValue(self, key, defaultValue): return Float(self.mSettings.value(key, defaultValue)) def stampsDirectory(self): if self.mStampsDirectory == '': appData = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation) return appData + "/stamps" return self.mStampsDirectory def setStampsDirectory(self, stampsDirectory): if self.mStampsDirectory == stampsDirectory: return self.mStampsDirectory = stampsDirectory self.mSettings.setValue("Storage/StampsDirectory", stampsDirectory) self.stampsDirectoryChanged.emit(stampsDirectory)
class TileLayer(Layer): ## # Constructor. ## def __init__(self, name, x, y, width, height): super().__init__(Layer.TileLayerType, name, x, y, width, height) self.mMaxTileSize = QSize(0, 0) self.mGrid = QVector() for i in range(width * height): self.mGrid.append(Cell()) self.mOffsetMargins = QMargins() def __iter__(self): return self.mGrid.__iter__() ## # Returns the maximum tile size of this layer. ## def maxTileSize(self): return self.mMaxTileSize ## # Returns the margins that have to be taken into account while drawing # this tile layer. The margins depend on the maximum tile size and the # offset applied to the tiles. ## def drawMargins(self): return QMargins( self.mOffsetMargins.left(), self.mOffsetMargins.top() + self.mMaxTileSize.height(), self.mOffsetMargins.right() + self.mMaxTileSize.width(), self.mOffsetMargins.bottom()) ## # Recomputes the draw margins. Needed after the tile offset of a tileset # has changed for example. # # Generally you want to call Map.recomputeDrawMargins instead. ## def recomputeDrawMargins(self): maxTileSize = QSize(0, 0) offsetMargins = QMargins() i = 0 while (i < self.mGrid.size()): cell = self.mGrid.at(i) tile = cell.tile if tile: size = tile.size() if (cell.flippedAntiDiagonally): size.transpose() offset = tile.offset() maxTileSize = maxSize(size, maxTileSize) offsetMargins = maxMargins( QMargins(-offset.x(), -offset.y(), offset.x(), offset.y()), offsetMargins) i += 1 self.mMaxTileSize = maxTileSize self.mOffsetMargins = offsetMargins if (self.mMap): self.mMap.adjustDrawMargins(self.drawMargins()) ## # Returns whether (x, y) is inside this map layer. ## def contains(self, *args): l = len(args) if l == 2: x, y = args return x >= 0 and y >= 0 and x < self.mWidth and y < self.mHeight elif l == 1: point = args[0] return self.contains(point.x(), point.y()) ## # Calculates the region of cells in this tile layer for which the given # \a condition returns True. ## def region(self, *args): l = len(args) if l == 1: condition = args[0] region = QRegion() for y in range(self.mHeight): for x in range(self.mWidth): if (condition(self.cellAt(x, y))): rangeStart = x x += 1 while (x <= self.mWidth): if (x == self.mWidth or not condition(self.cellAt(x, y))): rangeEnd = x region += QRect(rangeStart + self.mX, y + self.mY, rangeEnd - rangeStart, 1) break x += 1 return region elif l == 0: ## # Calculates the region occupied by the tiles of this layer. Similar to # Layer.bounds(), but leaves out the regions without tiles. ## return self.region(lambda cell: not cell.isEmpty()) ## # Returns a read-only reference to the cell at the given coordinates. The # coordinates have to be within this layer. ## def cellAt(self, *args): l = len(args) if l == 2: x, y = args return self.mGrid.at(x + y * self.mWidth) elif l == 1: point = args[0] return self.cellAt(point.x(), point.y()) ## # Sets the cell at the given coordinates. ## def setCell(self, x, y, cell): if (cell.tile): size = cell.tile.size() if (cell.flippedAntiDiagonally): size.transpose() offset = cell.tile.offset() self.mMaxTileSize = maxSize(size, self.mMaxTileSize) self.mOffsetMargins = maxMargins( QMargins(-offset.x(), -offset.y(), offset.x(), offset.y()), self.mOffsetMargins) if (self.mMap): self.mMap.adjustDrawMargins(self.drawMargins()) self.mGrid[x + y * self.mWidth] = cell ## # Returns a copy of the area specified by the given \a region. The # caller is responsible for the returned tile layer. ## def copy(self, *args): l = len(args) if l == 1: region = args[0] if type(region) != QRegion: region = QRegion(region) area = region.intersected(QRect(0, 0, self.width(), self.height())) bounds = region.boundingRect() areaBounds = area.boundingRect() offsetX = max(0, areaBounds.x() - bounds.x()) offsetY = max(0, areaBounds.y() - bounds.y()) copied = TileLayer(QString(), 0, 0, bounds.width(), bounds.height()) for rect in area.rects(): for x in range(rect.left(), rect.right() + 1): for y in range(rect.top(), rect.bottom() + 1): copied.setCell(x - areaBounds.x() + offsetX, y - areaBounds.y() + offsetY, self.cellAt(x, y)) return copied elif l == 4: x, y, width, height = args return self.copy(QRegion(x, y, width, height)) ## # Merges the given \a layer onto this layer at position \a pos. Parts that # fall outside of this layer will be lost and empty tiles in the given # layer will have no effect. ## def merge(self, pos, layer): # Determine the overlapping area area = QRect(pos, QSize(layer.width(), layer.height())) area &= QRect(0, 0, self.width(), self.height()) for y in range(area.top(), area.bottom() + 1): for x in range(area.left(), area.right() + 1): cell = layer.cellAt(x - pos.x(), y - pos.y()) if (not cell.isEmpty()): self.setCell(x, y, cell) ## # Removes all cells in the specified region. ## def erase(self, area): emptyCell = Cell() for rect in area.rects(): for x in range(rect.left(), rect.right() + 1): for y in range(rect.top(), rect.bottom() + 1): self.setCell(x, y, emptyCell) ## # Sets the cells starting at the given position to the cells in the given # \a tileLayer. Parts that fall outside of this layer will be ignored. # # When a \a mask is given, only cells that fall within this mask are set. # The mask is applied in local coordinates. ## def setCells(self, x, y, layer, mask=QRegion()): # Determine the overlapping area area = QRegion(QRect(x, y, layer.width(), layer.height())) area &= QRect(0, 0, self.width(), self.height()) if (not mask.isEmpty()): area &= mask for rect in area.rects(): for _x in range(rect.left(), rect.right() + 1): for _y in range(rect.top(), rect.bottom() + 1): self.setCell(_x, _y, layer.cellAt(_x - x, _y - y)) ## # Flip this tile layer in the given \a direction. Direction must be # horizontal or vertical. This doesn't change the dimensions of the # tile layer. ## def flip(self, direction): newGrid = QVector() for i in range(self.mWidth * self.mHeight): newGrid.append(Cell()) for y in range(self.mHeight): for x in range(self.mWidth): dest = newGrid[x + y * self.mWidth] if (direction == FlipDirection.FlipHorizontally): source = self.cellAt(self.mWidth - x - 1, y) dest = source dest.flippedHorizontally = not source.flippedHorizontally elif (direction == FlipDirection.FlipVertically): source = self.cellAt(x, self.mHeight - y - 1) dest = source dest.flippedVertically = not source.flippedVertically self.mGrid = newGrid ## # Rotate this tile layer by 90 degrees left or right. The tile positions # are rotated within the layer, and the tiles themselves are rotated. The # dimensions of the tile layer are swapped. ## def rotate(self, direction): rotateRightMask = [5, 4, 1, 0, 7, 6, 3, 2] rotateLeftMask = [3, 2, 7, 6, 1, 0, 5, 4] if direction == RotateDirection.RotateRight: rotateMask = rotateRightMask else: rotateMask = rotateLeftMask newWidth = self.mHeight newHeight = self.mWidth newGrid = QVector(newWidth * newHeight) for y in range(self.mHeight): for x in range(self.mWidth): source = self.cellAt(x, y) dest = source mask = (dest.flippedHorizontally << 2) | ( dest.flippedVertically << 1) | ( dest.flippedAntiDiagonally << 0) mask = rotateMask[mask] dest.flippedHorizontally = (mask & 4) != 0 dest.flippedVertically = (mask & 2) != 0 dest.flippedAntiDiagonally = (mask & 1) != 0 if (direction == RotateDirection.RotateRight): newGrid[x * newWidth + (self.mHeight - y - 1)] = dest else: newGrid[(self.mWidth - x - 1) * newWidth + y] = dest t = self.mMaxTileSize.width() self.mMaxTileSize.setWidth(self.mMaxTileSize.height()) self.mMaxTileSize.setHeight(t) self.mWidth = newWidth self.mHeight = newHeight self.mGrid = newGrid ## # Computes and returns the set of tilesets used by this tile layer. ## def usedTilesets(self): tilesets = QSet() i = 0 while (i < self.mGrid.size()): tile = self.mGrid.at(i).tile if tile: tilesets.insert(tile.tileset()) i += 1 return tilesets ## # Returns whether this tile layer has any cell for which the given # \a condition returns True. ## def hasCell(self, condition): i = 0 for cell in self.mGrid: if (condition(cell)): return True i += 1 return False ## # Returns whether this tile layer is referencing the given tileset. ## def referencesTileset(self, tileset): i = 0 while (i < self.mGrid.size()): tile = self.mGrid.at(i).tile if (tile and tile.tileset() == tileset): return True i += 1 return False ## # Removes all references to the given tileset. This sets all tiles on this # layer that are from the given tileset to null. ## def removeReferencesToTileset(self, tileset): i = 0 while (i < self.mGrid.size()): tile = self.mGrid.at(i).tile if (tile and tile.tileset() == tileset): self.mGrid.replace(i, Cell()) i += 1 ## # Replaces all tiles from \a oldTileset with tiles from \a newTileset. ## def replaceReferencesToTileset(self, oldTileset, newTileset): i = 0 while (i < self.mGrid.size()): tile = self.mGrid.at(i).tile if (tile and tile.tileset() == oldTileset): self.mGrid[i].tile = newTileset.tileAt(tile.id()) i += 1 ## # Resizes this tile layer to \a size, while shifting all tiles by # \a offset. ## def resize(self, size, offset): if (self.size() == size and offset.isNull()): return newGrid = QVector() for i in range(size.width() * size.height()): newGrid.append(Cell()) # Copy over the preserved part startX = max(0, -offset.x()) startY = max(0, -offset.y()) endX = min(self.mWidth, size.width() - offset.x()) endY = min(self.mHeight, size.height() - offset.y()) for y in range(startY, endY): for x in range(startX, endX): index = x + offset.x() + (y + offset.y()) * size.width() newGrid[index] = self.cellAt(x, y) self.mGrid = newGrid self.setSize(size) ## # Offsets the tiles in this layer within \a bounds by \a offset, # and optionally wraps them. # # \sa ObjectGroup.offset() ## def offsetTiles(self, offset, bounds, wrapX, wrapY): newGrid = QVector() for i in range(self.mWidth * self.mHeight): newGrid.append(Cell()) for y in range(self.mHeight): for x in range(self.mWidth): # Skip out of bounds tiles if (not bounds.contains(x, y)): newGrid[x + y * self.mWidth] = self.cellAt(x, y) continue # Get position to pull tile value from oldX = x - offset.x() oldY = y - offset.y() # Wrap x value that will be pulled from if (wrapX and bounds.width() > 0): while oldX < bounds.left(): oldX += bounds.width() while oldX > bounds.right(): oldX -= bounds.width() # Wrap y value that will be pulled from if (wrapY and bounds.height() > 0): while oldY < bounds.top(): oldY += bounds.height() while oldY > bounds.bottom(): oldY -= bounds.height() # Set the new tile if (self.contains(oldX, oldY) and bounds.contains(oldX, oldY)): newGrid[x + y * self.mWidth] = self.cellAt(oldX, oldY) else: newGrid[x + y * self.mWidth] = Cell() self.mGrid = newGrid def canMergeWith(self, other): return other.isTileLayer() def mergedWith(self, other): o = other unitedBounds = self.bounds().united(o.bounds()) offset = self.position() - unitedBounds.topLeft() merged = self.clone() merged.resize(unitedBounds.size(), offset) merged.merge(o.position() - unitedBounds.topLeft(), o) return merged ## # Returns the region where this tile layer and the given tile layer # are different. The relative positions of the layers are taken into # account. The returned region is relative to this tile layer. ## def computeDiffRegion(self, other): ret = QRegion() dx = other.x() - self.mX dy = other.y() - self.mY r = QRect(0, 0, self.width(), self.height()) r &= QRect(dx, dy, other.width(), other.height()) for y in range(r.top(), r.bottom() + 1): for x in range(r.left(), r.right() + 1): if (self.cellAt(x, y) != other.cellAt(x - dx, y - dy)): rangeStart = x while (x <= r.right() and self.cellAt(x, y) != other.cellAt(x - dx, y - dy)): x += 1 rangeEnd = x ret += QRect(rangeStart, y, rangeEnd - rangeStart, 1) return ret ## # Returns True if all tiles in the layer are empty. ## def isEmpty(self): i = 0 while (i < self.mGrid.size()): if (not self.mGrid.at(i).isEmpty()): return False i += 1 return True ## # Returns a duplicate of this TileLayer. # # \sa Layer.clone() ## def clone(self): return self.initializeClone( TileLayer(self.mName, self.mX, self.mY, self.mWidth, self.mHeight)) def begin(self): return self.mGrid.begin() def end(self): return self.mGrid.end() def initializeClone(self, clone): super().initializeClone(clone) clone.mGrid = self.mGrid clone.mMaxTileSize = self.mMaxTileSize clone.mOffsetMargins = self.mOffsetMargins return clone
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 CellRenderer(): BottomLeft, BottomCenter, TopLeft = range(3) def __init__(self, painter): self.mPainter = painter self.mTile = None self.mIsOpenGL = hasOpenGLEngine(painter) self.mFragments = QVector() def __del__(self): self.flush() ## # Renders a \a cell with the given \a origin at \a pos, taking into account # the flipping and tile offset. # # For performance reasons, the actual drawing is delayed until a different # kind of tile has to be drawn. For this reason it is necessary to call # flush when finished doing drawCell calls. This function is also called by # the destructor so usually an explicit call is not needed. ## def render(self, cell, pos, cellSize, origin): if (self.mTile != cell.tile): self.flush() image = cell.tile.currentFrameImage() size = image.size() if cellSize == QSizeF(0, 0): objectSize = size else: objectSize = cellSize scale = QSizeF(objectSize.width() / size.width(), objectSize.height() / size.height()) offset = cell.tile.offset() sizeHalf = QPointF(objectSize.width() / 2, objectSize.height() / 2) fragment = QPainter.PixmapFragment() fragment.x = pos.x() + (offset.x() * scale.width()) + sizeHalf.x() fragment.y = pos.y() + ( offset.y() * scale.height()) + sizeHalf.y() - objectSize.height() fragment.sourceLeft = 0 fragment.sourceTop = 0 fragment.width = size.width() fragment.height = size.height() if cell.flippedHorizontally: fragment.scaleX = -1 else: fragment.scaleX = 1 if cell.flippedVertically: fragment.scaleY = -1 else: fragment.scaleY = 1 fragment.rotation = 0 fragment.opacity = 1 flippedHorizontally = cell.flippedHorizontally flippedVertically = cell.flippedVertically if (origin == CellRenderer.BottomCenter): fragment.x -= sizeHalf.x() if (cell.flippedAntiDiagonally): fragment.rotation = 90 flippedHorizontally = cell.flippedVertically flippedVertically = not cell.flippedHorizontally # Compensate for the swap of image dimensions halfDiff = sizeHalf.y() - sizeHalf.x() fragment.y += halfDiff if (origin != CellRenderer.BottomCenter): fragment.x += halfDiff if flippedHorizontally: x = -1 else: x = 1 fragment.scaleX = scale.width() * x if flippedVertically: x = -1 else: x = 1 fragment.scaleY = scale.height() * x if (self.mIsOpenGL or (fragment.scaleX > 0 and fragment.scaleY > 0)): self.mTile = cell.tile self.mFragments.append(fragment) return # The Raster paint engine as of Qt 4.8.4 / 5.0.2 does not support # drawing fragments with a negative scaling factor. self.flush() # make sure we drew all tiles so far oldTransform = self.mPainter.transform() transform = oldTransform transform.translate(fragment.x, fragment.y) transform.rotate(fragment.rotation) transform.scale(fragment.scaleX, fragment.scaleY) target = QRectF(fragment.width * -0.5, fragment.height * -0.5, fragment.width, fragment.height) source = QRectF(0, 0, fragment.width, fragment.height) self.mPainter.setTransform(transform) self.mPainter.drawPixmap(target, image, source) self.mPainter.setTransform(oldTransform) def flush(self): if (not self.mTile): return self.mPainter.drawPixmapFragments(self.mFragments, self.mTile.currentFrameImage()) self.mTile = None self.mFragments.resize(0)
def __toTileset(self, variant): variantMap = variant firstGid = variantMap.get("firstgid", 0) # Handle external tilesets sourceVariant = variantMap.get("source", '') if sourceVariant != '': source = resolvePath(self.mMapDir, sourceVariant) tileset, error = readTileset(source) if not tileset: self.mError = self.tr("Error while loading tileset '%s': %s" % (source, error)) else: self.mGidMapper.insert(firstGid, tileset) return tileset name = variantMap.get("name", '') tileWidth = variantMap.get("tilewidth", 0) tileHeight = variantMap.get("tileheight", 0) spacing = variantMap.get("spacing", 0) margin = variantMap.get("margin", 0) tileOffset = variantMap.get("tileoffset", {}) tileOffsetX = tileOffset.get("x", 0) tileOffsetY = tileOffset.get("y", 0) if (tileWidth <= 0 or tileHeight <= 0 or (firstGid == 0 and not self.mReadingExternalTileset)): self.mError = self.tr( "Invalid tileset parameters for tileset '%s'" % name) return None tileset = Tileset.create(name, tileWidth, tileHeight, spacing, margin) tileset.setTileOffset(QPoint(tileOffsetX, tileOffsetY)) trans = variantMap.get("transparentcolor", '') if (trans != '' and QColor.isValidColor(trans)): tileset.setTransparentColor(QColor(trans)) imageVariant = variantMap.get("image", '') if imageVariant != '': imagePath = resolvePath(self.mMapDir, imageVariant) if (not tileset.loadFromImage(imagePath)): self.mError = self.tr("Error loading tileset image:\n'%s'" % imagePath) return None tileset.setProperties( self.toProperties(variantMap.get("properties", {}))) # Read terrains terrainsVariantList = variantMap.get("terrains", []) for terrainMap in terrainsVariantList: tileset.addTerrain(terrainMap.get("name", ''), terrainMap.get("tile", 0)) # Read tile terrain and external image information tilesVariantMap = variantMap.get("tiles", {}) for it in tilesVariantMap.items(): ok = False tileIndex = Int(it[0]) if (tileIndex < 0): self.mError = self.tr("Tileset tile index negative:\n'%d'" % tileIndex) if (tileIndex >= tileset.tileCount()): # Extend the tileset to fit the tile if (tileIndex >= len(tilesVariantMap)): # If tiles are defined this way, there should be an entry # for each tile. # Limit the index to number of entries to prevent running out # of memory on malicious input.f self.mError = self.tr( "Tileset tile index too high:\n'%d'" % tileIndex) return None for i in range(tileset.tileCount(), tileIndex + 1): tileset.addTile(QPixmap()) tile = tileset.tileAt(tileIndex) if (tile): tileVar = it[1] terrains = tileVar.get("terrain", []) if len(terrains) == 4: for i in range(0, 4): terrainId, ok = Int2(terrains[i]) if (ok and terrainId >= 0 and terrainId < tileset.terrainCount()): tile.setCornerTerrainId(i, terrainId) probability, ok = Float2(tileVar.get("probability", 0.0)) if (ok): tile.setProbability(probability) imageVariant = tileVar.get("image", '') if imageVariant != '': imagePath = resolvePath(self.mMapDir, imageVariant) tileset.setTileImage(tileIndex, QPixmap(imagePath), imagePath) objectGroupVariant = tileVar.get("objectgroup", {}) if len(objectGroupVariant) > 0: tile.setObjectGroup(self.toObjectGroup(objectGroupVariant)) frameList = tileVar.get("animation", []) lenFrames = len(frameList) if lenFrames > 0: frames = QVector() for i in range(lenFrames): frames.append(Frame()) for i in range(lenFrames - 1, -1, -1): frameVariantMap = frameList[i] frame = frames[i] frame.tileId = frameVariantMap.get("tileid", 0) frame.duration = frameVariantMap.get("duration", 0) tile.setFrames(frames) # Read tile properties propertiesVariantMap = variantMap.get("tileproperties", {}) for it in propertiesVariantMap.items(): tileIndex = Int(it[0]) propertiesVar = it[1] if (tileIndex >= 0 and tileIndex < tileset.tileCount()): properties = self.toProperties(propertiesVar) tileset.tileAt(tileIndex).setProperties(properties) if not self.mReadingExternalTileset: self.mGidMapper.insert(firstGid, tileset) return tileset
def fillRegion(layer, fillOrigin): # Create that region that will hold the fill fillRegion = QRegion() # Silently quit if parameters are unsatisfactory if (not layer.contains(fillOrigin)): return fillRegion # Cache cell that we will match other cells against matchCell = layer.cellAt(fillOrigin) # Grab map dimensions for later use. layerWidth = layer.width() layerHeight = layer.height() layerSize = layerWidth * layerHeight # Create a queue to hold cells that need filling fillPositions = QList() fillPositions.append(fillOrigin) # Create an array that will store which cells have been processed # This is faster than checking if a given cell is in the region/list processedCellsVec = QVector() for i in range(layerSize): processedCellsVec.append(0xff) processedCells = processedCellsVec # Loop through queued positions and fill them, while at the same time # checking adjacent positions to see if they should be added while (not fillPositions.empty()): currentPoint = fillPositions.takeFirst() startOfLine = currentPoint.y() * layerWidth # Seek as far left as we can left = currentPoint.x() while (left > 0 and layer.cellAt(left - 1, currentPoint.y()) == matchCell): left -= 1 # Seek as far right as we can right = currentPoint.x() while (right + 1 < layerWidth and layer.cellAt(right + 1, currentPoint.y()) == matchCell): right += 1 # Add cells between left and right to the region fillRegion += QRegion(left, currentPoint.y(), right - left + 1, 1) # Add cell strip to processed cells for i in range(startOfLine + left, right + startOfLine, 1): processedCells[i] = 1 # These variables cache whether the last cell was added to the queue # or not as an optimization, since adjacent cells on the x axis # do not need to be added to the queue. lastAboveCell = False lastBelowCell = False # Loop between left and right and check if cells above or # below need to be added to the queue for x in range(left, right+1): fillPoint = QPoint(x, currentPoint.y()) # Check cell above if (fillPoint.y() > 0): aboveCell = QPoint(fillPoint.x(), fillPoint.y() - 1) if (not processedCells[aboveCell.y() * layerWidth + aboveCell.x()] and layer.cellAt(aboveCell) == matchCell): # Do not add the above cell to the queue if its # x-adjacent cell was added. if (not lastAboveCell): fillPositions.append(aboveCell) lastAboveCell = True else: lastAboveCell = False processedCells[aboveCell.y() * layerWidth + aboveCell.x()] = 1 # Check cell below if (fillPoint.y() + 1 < layerHeight): belowCell = QPoint(fillPoint.x(), fillPoint.y() + 1) if (not processedCells[belowCell.y() * layerWidth + belowCell.x()] and layer.cellAt(belowCell) == matchCell): # Do not add the below cell to the queue if its # x-adjacent cell was added. if (not lastBelowCell): fillPositions.append(belowCell) lastBelowCell = True else: lastBelowCell = False processedCells[belowCell.y() * layerWidth + belowCell.x()] = 1 return fillRegion
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
class ObjectTypesModel(QAbstractTableModel): ColorRole = Qt.UserRole def __init__(self, parent): super().__init__(parent) self.mObjectTypes = QVector() def setObjectTypes(self, objectTypes): self.beginResetModel() self.mObjectTypes = objectTypes self.endResetModel() def objectTypes(self): return self.mObjectTypes def rowCount(self, parent): if parent.isValid(): _x = 0 else: _x = self.mObjectTypes.size() return _x def columnCount(self, parent): if parent.isValid(): _x = 0 else: _x = 2 return _x def headerData(self, section, orientation, role): if (orientation == Qt.Horizontal): if (role == Qt.DisplayRole): x = section if x==0: return self.tr("Type") elif x==1: return self.tr("Color") elif (role == Qt.TextAlignmentRole): return Qt.AlignLeft return QVariant() def data(self, index, role): # QComboBox requests data for an invalid index when the model is empty if (not index.isValid()): return QVariant() objectType = self.mObjectTypes.at(index.row()) if (role == Qt.DisplayRole or role == Qt.EditRole): if (index.column() == 0): return objectType.name if (role == ObjectTypesModel.ColorRole and index.column() == 1): return objectType.color return QVariant() def setData(self, index, value, role): if (role == Qt.EditRole and index.column() == 0): self.mObjectTypes[index.row()].name = value.strip() self.dataChanged.emit(index, index) return True return False def flags(self, index): f = super().flags(index) if (index.column() == 0): f |= Qt.ItemIsEditable return f def setObjectTypeColor(self, objectIndex, color): self.mObjectTypes[objectIndex].color = color mi = self.index(objectIndex, 1) self.dataChanged.emit(mi, mi) def removeObjectTypes(self, indexes): rows = QVector() for index in indexes: rows.append(index.row()) rows = sorted(rows) for i in range(len(rows) - 1, -1, -1): row = rows[i] self.beginRemoveRows(QModelIndex(), row, row) self.mObjectTypes.remove(row) self.endRemoveRows() def appendNewObjectType(self): self.beginInsertRows(QModelIndex(), self.mObjectTypes.size(), self.mObjectTypes.size()) self.mObjectTypes.append(ObjectType()) self.endInsertRows()
def __readTilesetTile(self, tileset): atts = self.xml.attributes() id = Int(atts.value("id")) if (id < 0): self.xml.raiseError(self.tr("Invalid tile ID: %d"%id)) return hasImage = tileset.imageSource()!='' if (hasImage and id >= tileset.tileCount()): self.xml.raiseError(self.tr("Tile ID does not exist in tileset image: %d"%id)) return if (id > tileset.tileCount()): self.xml.raiseError(self.tr("Invalid (nonconsecutive) tile ID: %d"%id)) return # For tilesets without image source, consecutive tile IDs are allowed (for # tiles with individual images) if (id == tileset.tileCount()): tileset.addTile(QPixmap()) tile = tileset.tileAt(id) # Read tile quadrant terrain ids terrain = atts.value("terrain") if terrain != '': quadrants = terrain.split(",") if (len(quadrants) == 4): for i in range(4): if quadrants[i]=='': t = -1 else: t = Int(quadrants[i]) tile.setCornerTerrainId(i, t) # Read tile probability probability = atts.value("probability") if probability != '': tile.setProbability(Float(probability)) while (self.xml.readNextStartElement()): if (self.xml.name() == "properties"): tile.mergeProperties(self.__readProperties()) elif (self.xml.name() == "image"): source = self.xml.attributes().value("source") if source != '': source = self.p.resolveReference(source, self.mPath) tileset.setTileImage(id, QPixmap.fromImage(self.__readImage()), source) elif (self.xml.name() == "objectgroup"): tile.setObjectGroup(self.__readObjectGroup()) elif (self.xml.name() == "animation"): tile.setFrames(self.__readAnimationFrames()) else: self.__readUnknownElement() # Temporary code to support TMW-style animation frame properties if (not tile.isAnimated() and tile.hasProperty("animation-frame0")): frames = QVector() i = 0 while(i>=0): frameName = "animation-frame" + str(i) delayName = "animation-delay" + str(i) if (tile.hasProperty(frameName) and tile.hasProperty(delayName)): frame = Frame() frame.tileId = tile.property(frameName) frame.duration = tile.property(delayName) * 10 frames.append(frame) else: break i += 1 tile.setFrames(frames)
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 CommandLineParser(): def __init__(self): self.mCurrentProgramName = QString() self.mOptions = QVector() self.mShowHelp = False self.mLongestArgument = 0 self.mFilesToOpen = QStringList() def tr(self, sourceText, disambiguation = '', n = -1): return QCoreApplication.translate('CommandLineParser', sourceText, disambiguation, n) def trUtf8(self, sourceText, disambiguation = '', n = -1): return QCoreApplication.translate('CommandLineParser', sourceText, disambiguation, n) ## # Registers an option with the parser. When an option with the given # \a shortName or \a longName is encountered, \a callback is called with # \a data as its only parameter. ## def registerOption(self, *args): l = len(args) if l==4: ## # Convenience overload that allows registering an option with a callback # as a member function of a class. The class type and the member function # are given as template parameters, while the instance is passed in as # \a handler. # # \overload ## handler, shortName, longName, help = args self.registerOption(MemberFunctionCall, handler, shortName, longName, help) elif l==5: callback, data, shortName, longName, help = args self.mOptions.append(CommandLineParser.Option(callback, data, shortName, longName, help)) length = longName.length() if (self.mLongestArgument < length): self.mLongestArgument = length ## # Parses the given \a arguments. Returns False when the application is not # expected to run (either there was a parsing error, or the help was # requested). ## def parse(self, arguments): self.mFilesToOpen.clear() self.mShowHelp = False todo = QStringList(arguments) self.mCurrentProgramName = QFileInfo(todo.takeFirst()).fileName() index = 0 noMoreArguments = False while (not todo.isEmpty()): index += 1 arg = todo.takeFirst() if (arg.isEmpty()): continue if (noMoreArguments or arg.at(0) != '-'): self.mFilesToOpen.append(arg) continue if (arg.length() == 1): # Traditionally a single hyphen means read file from stdin, # write file to stdout. This isn't supported right now. qWarning(self.tr("Bad argument %d: lonely hyphen"%index)) self.showHelp() return False # Long options if (arg.at(1) == '-'): # Double hypen "--" means no more options will follow if (arg.length() == 2): noMoreArguments = True continue if (not self.handleLongOption(arg)): qWarning(self.tr("Unknown long argument %d: %s"%(index, arg))) self.mShowHelp = True break continue # Short options for i in range(1, arg.length()): c = arg.at(i) if (not self.handleShortOption(c)): qWarning(self.tr("Unknown short argument %d.%d: %s"%(index, i, c))) self.mShowHelp = True break if (self.mShowHelp): self.showHelp() return False return True ## # Returns the files to open that were found among the arguments. ## def filesToOpen(self): return QList(self.mFilesToOpen) def showHelp(self): qWarning(self.tr("Usage:\n %s [options] [files...]"%self.mCurrentProgramName) + "\n\n" + self.tr("Options:")) qWarning(" -h %-*s : %s", self.mLongestArgument, "--help", self.tr("Display this help")) for option in self.mOptions: if (not option.shortName.isNull()): qWarning(" -%c %-*s : %s", option.shortName.toLatin1(), self.mLongestArgument, option.longName, option.help) else: qWarning(" %-*s : %s", self.mLongestArgument, option.longName, option.help) qWarning() def handleLongOption(self, longName): if (longName == "--help"): self.mShowHelp = True return True for option in self.mOptions: if (longName == option.longName): option.callback(option.data) return True return False def handleShortOption(self, c): if (c == 'h'): self.mShowHelp = True return True for option in self.mOptions: if (c == option.shortName): option.callback(option.data) return True return False ## # Internal definition of a command line option. ## class Option(): def __init__(self, *args): l = len(args) callback = Callback() shortName = QChar() longName = QString() help = QString() if l==0: self.callback = 0 self.data = 0 elif l==5: callback = args[0] data = args[1] shortName = args[2] longName = args[3] help = args[4] self.callback = callback self.data = data self.shortName = shortName self.longName = longName self.help = help
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
def drawGrid(self, painter, exposed, gridColor): rect = exposed.toAlignedRect() if (rect.isNull()): return p = RenderParams(self.map()) # Determine the tile and pixel coordinates to start at startTile = self.screenToTileCoords_(rect.topLeft()).toPoint() startPos = self.tileToScreenCoords_(startTile).toPoint() ## Determine in which half of the tile the top-left corner of the area we # need to draw is. If we're in the upper half, we need to start one row # up due to those tiles being visible as well. How we go up one row # depends on whether we're in the left or right half of the tile. ## inUpperHalf = rect.y() - startPos.y() < p.sideOffsetY inLeftHalf = rect.x() - startPos.x() < p.sideOffsetX if (inUpperHalf): startTile.setY(startTile.y() - 1) if (inLeftHalf): startTile.setX(startTile.x() - 1) startTile.setX(max(0, startTile.x())) startTile.setY(max(0, startTile.y())) startPos = self.tileToScreenCoords_(startTile).toPoint() oct = [ QPoint(0, p.tileHeight - p.sideOffsetY), QPoint(0, p.sideOffsetY), QPoint(p.sideOffsetX, 0), QPoint(p.tileWidth - p.sideOffsetX, 0), QPoint(p.tileWidth, p.sideOffsetY), QPoint(p.tileWidth, p.tileHeight - p.sideOffsetY), QPoint(p.tileWidth - p.sideOffsetX, p.tileHeight), QPoint(p.sideOffsetX, p.tileHeight)] lines = QVector() #lines.reserve(8) gridColor.setAlpha(128) gridPen = QPen(gridColor) gridPen.setCosmetic(True) _x = QVector() _x.append(2) _x.append(2) gridPen.setDashPattern(_x) painter.setPen(gridPen) if (p.staggerX): # Odd row shifting is applied in the rendering loop, so un-apply it here if (p.doStaggerX(startTile.x())): startPos.setY(startPos.y() - p.rowHeight) while(startPos.x() <= rect.right() and startTile.x() < self.map().width()): rowTile = QPoint(startTile) rowPos = QPoint(startPos) if (p.doStaggerX(startTile.x())): rowPos.setY(rowPos.y() + p.rowHeight) while(rowPos.y() <= rect.bottom() and rowTile.y() < self.map().height()): lines.append(QLineF(rowPos + oct[1], rowPos + oct[2])) lines.append(QLineF(rowPos + oct[2], rowPos + oct[3])) lines.append(QLineF(rowPos + oct[3], rowPos + oct[4])) isStaggered = p.doStaggerX(startTile.x()) lastRow = rowTile.y() == self.map().height() - 1 lastColumn = rowTile.x() == self.map().width() - 1 bottomLeft = rowTile.x() == 0 or (lastRow and isStaggered) bottomRight = lastColumn or (lastRow and isStaggered) if (bottomRight): lines.append(QLineF(rowPos + oct[5], rowPos + oct[6])) if (lastRow): lines.append(QLineF(rowPos + oct[6], rowPos + oct[7])) if (bottomLeft): lines.append(QLineF(rowPos + oct[7], rowPos + oct[0])) painter.drawLines(lines) lines.resize(0) rowPos.setY(rowPos.y() + p.tileHeight + p.sideLengthY) rowTile.setY(rowTile.y() + 1) startPos.setX(startPos.x() + p.columnWidth) startTile.setX(startTile.x() + 1) else: # Odd row shifting is applied in the rendering loop, so un-apply it here if (p.doStaggerY(startTile.y())): startPos.setX(startPos.x() - p.columnWidth) while(startPos.y() <= rect.bottom() and startTile.y() < self.map().height()): rowTile = QPoint(startTile) rowPos = QPoint(startPos) if (p.doStaggerY(startTile.y())): rowPos.setX(rowPos.x() + p.columnWidth) while(rowPos.x() <= rect.right() and rowTile.x() < self.map().width()): lines.append(QLineF(rowPos + oct[0], rowPos + oct[1])) lines.append(QLineF(rowPos + oct[1], rowPos + oct[2])) lines.append(QLineF(rowPos + oct[3], rowPos + oct[4])) isStaggered = p.doStaggerY(startTile.y()) lastRow = rowTile.y() == self.map().height() - 1 lastColumn = rowTile.x() == self.map().width() - 1 bottomLeft = lastRow or (rowTile.x() == 0 and not isStaggered) bottomRight = lastRow or (lastColumn and isStaggered) if (lastColumn): lines.append(QLineF(rowPos + oct[4], rowPos + oct[5])) if (bottomRight): lines.append(QLineF(rowPos + oct[5], rowPos + oct[6])) if (bottomLeft): lines.append(QLineF(rowPos + oct[7], rowPos + oct[0])) painter.drawLines(lines) lines.resize(0) rowPos.setX(rowPos.x() + p.tileWidth + p.sideLengthX) rowTile.setX(rowTile.x() + 1) startPos.setY(startPos.y() + p.rowHeight) startTile.setY(startTile.y() + 1)
def recalculateTerrainDistances(self): # some fancy macros which can search for a value in each byte of a word simultaneously def hasZeroByte(dword): return (dword - 0x01010101) & ~dword & 0x80808080 def hasByteEqualTo(dword, value): return hasZeroByte(dword ^ int(~0/255 * value)) # Terrain distances are the number of transitions required before one terrain may meet another # Terrains that have no transition path have a distance of -1 for i in range(self.terrainCount()): type = self.terrain(i) distance = QVector() for _x in range(self.terrainCount() + 1): distance.append(-1) # Check all tiles for transitions to other terrain types for j in range(self.tileCount()): t = self.tileAt(j) if (not hasByteEqualTo(t.terrain(), i)): continue # This tile has transitions, add the transitions as neightbours (distance 1) tl = t.cornerTerrainId(0) tr = t.cornerTerrainId(1) bl = t.cornerTerrainId(2) br = t.cornerTerrainId(3) # Terrain on diagonally opposite corners are not actually a neighbour if (tl == i or br == i): distance[tr + 1] = 1 distance[bl + 1] = 1 if (tr == i or bl == i): distance[tl + 1] = 1 distance[br + 1] = 1 # terrain has at least one tile of its own type distance[i + 1] = 0 type.setTransitionDistances(distance) # Calculate indirect transition distances bNewConnections = False # Repeat while we are still making new connections (could take a # number of iterations for distant terrain types to connect) while bNewConnections: bNewConnections = False # For each combination of terrain types for i in range(self.terrainCount()): t0 = self.terrain(i) for j in range(self.terrainCount()): if (i == j): continue t1 = self.terrain(j) # Scan through each terrain type, and see if we have any in common for t in range(-1, self.terrainCount()): d0 = t0.transitionDistance(t) d1 = t1.transitionDistance(t) if (d0 == -1 or d1 == -1): continue # We have cound a common connection d = t0.transitionDistance(j) # If the new path is shorter, record the new distance if (d == -1 or d0 + d1 < d): d = d0 + d1 t0.setTransitionDistance(j, d) t1.setTransitionDistance(i, d) # We're making progress, flag for another iteration... bNewConnections = True
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 TileLayer(Layer): ## # Constructor. ## def __init__(self, name, x, y, width, height): super().__init__(Layer.TileLayerType, name, x, y, width, height) self.mMaxTileSize = QSize(0, 0) self.mGrid = QVector() for i in range(width * height): self.mGrid.append(Cell()) self.mOffsetMargins = QMargins() def __iter__(self): return self.mGrid.__iter__() ## # Returns the maximum tile size of this layer. ## def maxTileSize(self): return self.mMaxTileSize ## # Returns the margins that have to be taken into account while drawing # this tile layer. The margins depend on the maximum tile size and the # offset applied to the tiles. ## def drawMargins(self): return QMargins(self.mOffsetMargins.left(), self.mOffsetMargins.top() + self.mMaxTileSize.height(), self.mOffsetMargins.right() + self.mMaxTileSize.width(), self.mOffsetMargins.bottom()) ## # Recomputes the draw margins. Needed after the tile offset of a tileset # has changed for example. # # Generally you want to call Map.recomputeDrawMargins instead. ## def recomputeDrawMargins(self): maxTileSize = QSize(0, 0) offsetMargins = QMargins() i = 0 while(i<self.mGrid.size()): cell = self.mGrid.at(i) tile = cell.tile if tile: size = tile.size() if (cell.flippedAntiDiagonally): size.transpose() offset = tile.offset() maxTileSize = maxSize(size, maxTileSize) offsetMargins = maxMargins(QMargins(-offset.x(), -offset.y(), offset.x(), offset.y()), offsetMargins) i += 1 self.mMaxTileSize = maxTileSize self.mOffsetMargins = offsetMargins if (self.mMap): self.mMap.adjustDrawMargins(self.drawMargins()) ## # Returns whether (x, y) is inside this map layer. ## def contains(self, *args): l = len(args) if l==2: x, y = args return x >= 0 and y >= 0 and x < self.mWidth and y < self.mHeight elif l==1: point = args[0] return self.contains(point.x(), point.y()) ## # Calculates the region of cells in this tile layer for which the given # \a condition returns True. ## def region(self, *args): l = len(args) if l==1: condition = args[0] region = QRegion() for y in range(self.mHeight): for x in range(self.mWidth): if (condition(self.cellAt(x, y))): rangeStart = x x += 1 while(x<=self.mWidth): if (x == self.mWidth or not condition(self.cellAt(x, y))): rangeEnd = x region += QRect(rangeStart + self.mX, y + self.mY, rangeEnd - rangeStart, 1) break x += 1 return region elif l==0: ## # Calculates the region occupied by the tiles of this layer. Similar to # Layer.bounds(), but leaves out the regions without tiles. ## return self.region(lambda cell:not cell.isEmpty()) ## # Returns a read-only reference to the cell at the given coordinates. The # coordinates have to be within this layer. ## def cellAt(self, *args): l = len(args) if l==2: x, y = args return self.mGrid.at(x + y * self.mWidth) elif l==1: point = args[0] return self.cellAt(point.x(), point.y()) ## # Sets the cell at the given coordinates. ## def setCell(self, x, y, cell): if (cell.tile): size = cell.tile.size() if (cell.flippedAntiDiagonally): size.transpose() offset = cell.tile.offset() self.mMaxTileSize = maxSize(size, self.mMaxTileSize) self.mOffsetMargins = maxMargins(QMargins(-offset.x(), -offset.y(), offset.x(), offset.y()), self.mOffsetMargins) if (self.mMap): self.mMap.adjustDrawMargins(self.drawMargins()) self.mGrid[x + y * self.mWidth] = cell ## # Returns a copy of the area specified by the given \a region. The # caller is responsible for the returned tile layer. ## def copy(self, *args): l = len(args) if l==1: region = args[0] if type(region) != QRegion: region = QRegion(region) area = region.intersected(QRect(0, 0, self.width(), self.height())) bounds = region.boundingRect() areaBounds = area.boundingRect() offsetX = max(0, areaBounds.x() - bounds.x()) offsetY = max(0, areaBounds.y() - bounds.y()) copied = TileLayer(QString(), 0, 0, bounds.width(), bounds.height()) for rect in area.rects(): for x in range(rect.left(), rect.right()+1): for y in range(rect.top(), rect.bottom()+1): copied.setCell(x - areaBounds.x() + offsetX, y - areaBounds.y() + offsetY, self.cellAt(x, y)) return copied elif l==4: x, y, width, height = args return self.copy(QRegion(x, y, width, height)) ## # Merges the given \a layer onto this layer at position \a pos. Parts that # fall outside of this layer will be lost and empty tiles in the given # layer will have no effect. ## def merge(self, pos, layer): # Determine the overlapping area area = QRect(pos, QSize(layer.width(), layer.height())) area &= QRect(0, 0, self.width(), self.height()) for y in range(area.top(), area.bottom()+1): for x in range(area.left(), area.right()+1): cell = layer.cellAt(x - pos.x(), y - pos.y()) if (not cell.isEmpty()): self.setCell(x, y, cell) ## # Removes all cells in the specified region. ## def erase(self, area): emptyCell = Cell() for rect in area.rects(): for x in range(rect.left(), rect.right()+1): for y in range(rect.top(), rect.bottom()+1): self.setCell(x, y, emptyCell) ## # Sets the cells starting at the given position to the cells in the given # \a tileLayer. Parts that fall outside of this layer will be ignored. # # When a \a mask is given, only cells that fall within this mask are set. # The mask is applied in local coordinates. ## def setCells(self, x, y, layer, mask = QRegion()): # Determine the overlapping area area = QRegion(QRect(x, y, layer.width(), layer.height())) area &= QRect(0, 0, self.width(), self.height()) if (not mask.isEmpty()): area &= mask for rect in area.rects(): for _x in range(rect.left(), rect.right()+1): for _y in range(rect.top(), rect.bottom()+1): self.setCell(_x, _y, layer.cellAt(_x - x, _y - y)) ## # Flip this tile layer in the given \a direction. Direction must be # horizontal or vertical. This doesn't change the dimensions of the # tile layer. ## def flip(self, direction): newGrid = QVector() for i in range(self.mWidth * self.mHeight): newGrid.append(Cell()) for y in range(self.mHeight): for x in range(self.mWidth): dest = newGrid[x + y * self.mWidth] if (direction == FlipDirection.FlipHorizontally): source = self.cellAt(self.mWidth - x - 1, y) dest = source dest.flippedHorizontally = not source.flippedHorizontally elif (direction == FlipDirection.FlipVertically): source = self.cellAt(x, self.mHeight - y - 1) dest = source dest.flippedVertically = not source.flippedVertically self.mGrid = newGrid ## # Rotate this tile layer by 90 degrees left or right. The tile positions # are rotated within the layer, and the tiles themselves are rotated. The # dimensions of the tile layer are swapped. ## def rotate(self, direction): rotateRightMask = [5, 4, 1, 0, 7, 6, 3, 2] rotateLeftMask = [3, 2, 7, 6, 1, 0, 5, 4] if direction == RotateDirection.RotateRight: rotateMask = rotateRightMask else: rotateMask = rotateLeftMask newWidth = self.mHeight newHeight = self.mWidth newGrid = QVector(newWidth * newHeight) for y in range(self.mHeight): for x in range(self.mWidth): source = self.cellAt(x, y) dest = source mask = (dest.flippedHorizontally << 2) | (dest.flippedVertically << 1) | (dest.flippedAntiDiagonally << 0) mask = rotateMask[mask] dest.flippedHorizontally = (mask & 4) != 0 dest.flippedVertically = (mask & 2) != 0 dest.flippedAntiDiagonally = (mask & 1) != 0 if (direction == RotateDirection.RotateRight): newGrid[x * newWidth + (self.mHeight - y - 1)] = dest else: newGrid[(self.mWidth - x - 1) * newWidth + y] = dest t = self.mMaxTileSize.width() self.mMaxTileSize.setWidth(self.mMaxTileSize.height()) self.mMaxTileSize.setHeight(t) self.mWidth = newWidth self.mHeight = newHeight self.mGrid = newGrid ## # Computes and returns the set of tilesets used by this tile layer. ## def usedTilesets(self): tilesets = QSet() i = 0 while(i<self.mGrid.size()): tile = self.mGrid.at(i).tile if tile: tilesets.insert(tile.tileset()) i += 1 return tilesets ## # Returns whether this tile layer has any cell for which the given # \a condition returns True. ## def hasCell(self, condition): i = 0 for cell in self.mGrid: if (condition(cell)): return True i += 1 return False ## # Returns whether this tile layer is referencing the given tileset. ## def referencesTileset(self, tileset): i = 0 while(i<self.mGrid.size()): tile = self.mGrid.at(i).tile if (tile and tile.tileset() == tileset): return True i += 1 return False ## # Removes all references to the given tileset. This sets all tiles on this # layer that are from the given tileset to null. ## def removeReferencesToTileset(self, tileset): i = 0 while(i<self.mGrid.size()): tile = self.mGrid.at(i).tile if (tile and tile.tileset() == tileset): self.mGrid.replace(i, Cell()) i += 1 ## # Replaces all tiles from \a oldTileset with tiles from \a newTileset. ## def replaceReferencesToTileset(self, oldTileset, newTileset): i = 0 while(i<self.mGrid.size()): tile = self.mGrid.at(i).tile if (tile and tile.tileset() == oldTileset): self.mGrid[i].tile = newTileset.tileAt(tile.id()) i += 1 ## # Resizes this tile layer to \a size, while shifting all tiles by # \a offset. ## def resize(self, size, offset): if (self.size() == size and offset.isNull()): return newGrid = QVector() for i in range(size.width() * size.height()): newGrid.append(Cell()) # Copy over the preserved part startX = max(0, -offset.x()) startY = max(0, -offset.y()) endX = min(self.mWidth, size.width() - offset.x()) endY = min(self.mHeight, size.height() - offset.y()) for y in range(startY, endY): for x in range(startX, endX): index = x + offset.x() + (y + offset.y()) * size.width() newGrid[index] = self.cellAt(x, y) self.mGrid = newGrid self.setSize(size) ## # Offsets the tiles in this layer within \a bounds by \a offset, # and optionally wraps them. # # \sa ObjectGroup.offset() ## def offsetTiles(self, offset, bounds, wrapX, wrapY): newGrid = QVector() for i in range(self.mWidth * self.mHeight): newGrid.append(Cell()) for y in range(self.mHeight): for x in range(self.mWidth): # Skip out of bounds tiles if (not bounds.contains(x, y)): newGrid[x + y * self.mWidth] = self.cellAt(x, y) continue # Get position to pull tile value from oldX = x - offset.x() oldY = y - offset.y() # Wrap x value that will be pulled from if (wrapX and bounds.width() > 0): while oldX < bounds.left(): oldX += bounds.width() while oldX > bounds.right(): oldX -= bounds.width() # Wrap y value that will be pulled from if (wrapY and bounds.height() > 0): while oldY < bounds.top(): oldY += bounds.height() while oldY > bounds.bottom(): oldY -= bounds.height() # Set the new tile if (self.contains(oldX, oldY) and bounds.contains(oldX, oldY)): newGrid[x + y * self.mWidth] = self.cellAt(oldX, oldY) else: newGrid[x + y * self.mWidth] = Cell() self.mGrid = newGrid def canMergeWith(self, other): return other.isTileLayer() def mergedWith(self, other): o = other unitedBounds = self.bounds().united(o.bounds()) offset = self.position() - unitedBounds.topLeft() merged = self.clone() merged.resize(unitedBounds.size(), offset) merged.merge(o.position() - unitedBounds.topLeft(), o) return merged ## # Returns the region where this tile layer and the given tile layer # are different. The relative positions of the layers are taken into # account. The returned region is relative to this tile layer. ## def computeDiffRegion(self, other): ret = QRegion() dx = other.x() - self.mX dy = other.y() - self.mY r = QRect(0, 0, self.width(), self.height()) r &= QRect(dx, dy, other.width(), other.height()) for y in range(r.top(), r.bottom()+1): for x in range(r.left(), r.right()+1): if (self.cellAt(x, y) != other.cellAt(x - dx, y - dy)): rangeStart = x while (x <= r.right() and self.cellAt(x, y) != other.cellAt(x - dx, y - dy)): x += 1 rangeEnd = x ret += QRect(rangeStart, y, rangeEnd - rangeStart, 1) return ret ## # Returns True if all tiles in the layer are empty. ## def isEmpty(self): i = 0 while(i<self.mGrid.size()): if (not self.mGrid.at(i).isEmpty()): return False i += 1 return True ## # Returns a duplicate of this TileLayer. # # \sa Layer.clone() ## def clone(self): return self.initializeClone(TileLayer(self.mName, self.mX, self.mY, self.mWidth, self.mHeight)) def begin(self): return self.mGrid.begin() def end(self): return self.mGrid.end() def initializeClone(self, clone): super().initializeClone(clone) clone.mGrid = self.mGrid clone.mMaxTileSize = self.mMaxTileSize clone.mOffsetMargins = self.mOffsetMargins return clone
class Zoomable(QObject): scaleChanged = pyqtSignal(float) def __init__(self, parent=None): super().__init__(parent) self.mScale = 1 self.mZoomFactors = QVector() self.mGestureStartScale = 0 self.mComboBox = None self.mComboRegExp = QRegExp("^\\s*(\\d+)\\s*%?\\s*$") self.mComboValidator = None for i in range(zoomFactorCount): self.mZoomFactors.append(zoomFactors[i]) def setScale(self, scale): if (scale == self.mScale): return self.mScale = scale self.syncComboBox() self.scaleChanged.emit(self.mScale) def scale(self): return self.mScale def canZoomIn(self): return self.mScale < self.mZoomFactors.last() def canZoomOut(self): return self.mScale > self.mZoomFactors.first() ## # Changes the current scale based on the given mouse wheel \a delta. # # For convenience, the delta is assumed to be in the same units as # QWheelEvent.delta, which is the distance that the wheel is rotated, # in eighths of a degree. ## def handleWheelDelta(self, delta): if (delta <= -120): self.zoomOut() elif (delta >= 120): self.zoomIn() else: # We're dealing with a finer-resolution mouse. Allow it to have finer # control over the zoom level. factor = 1 + 0.3 * qAbs(delta / 8 / 15) if (delta < 0): factor = 1 / factor scale = qBound(self.mZoomFactors.first(), self.mScale * factor, self.mZoomFactors.back()) # Round to at most four digits after the decimal point self.setScale(math.floor(scale * 10000 + 0.5) / 10000) ## # Changes the current scale based on the given pinch gesture. ## def handlePinchGesture(self, pinch): if (not (pinch.changeFlags() & QPinchGesture.ScaleFactorChanged)): return x = pinch.state() if x == Qt.NoGesture: pass elif x == Qt.GestureStarted: self.mGestureStartScale = self.mScale # fall through elif x == Qt.GestureUpdated: factor = pinch.scaleFactor() scale = qBound(self.mZoomFactors.first(), self.mGestureStartScale * factor, self.mZoomFactors.back()) self.setScale(math.floor(scale * 10000 + 0.5) / 10000) elif x == Qt.GestureFinished: pass elif x == Qt.GestureCanceled: pass ## # Returns whether images should be smoothly transformed when drawn at the # current scale. This is the case when the scale is not 1 and smaller than # 2. ## def smoothTransform(self): return self.mScale != 1.0 and self.mScale < 2.0 def setZoomFactors(self, factors): self.mZoomFactors = factors def connectToComboBox(self, comboBox): if (self.mComboBox): self.mComboBox.disconnect() if (self.mComboBox.lineEdit()): self.mComboBox.lineEdit().disconnect() self.mComboBox.setValidator(None) self.mComboBox = comboBox if type(comboBox) is QComboBox: self.mComboBox.clear() for scale in self.mZoomFactors: self.mComboBox.addItem(scaleToString(scale), scale) self.syncComboBox() self.mComboBox.activated.connect(self.comboActivated) self.mComboBox.setEditable(True) self.mComboBox.setInsertPolicy(QComboBox.NoInsert) self.mComboBox.lineEdit().editingFinished.connect(self.comboEdited) if (not self.mComboValidator): self.mComboValidator = QRegExpValidator( self.mComboRegExp, self) self.mComboBox.setValidator(self.mComboValidator) def zoomIn(self): for scale in self.mZoomFactors: if (scale > self.mScale): self.setScale(scale) break def zoomOut(self): for i in range(self.mZoomFactors.count() - 1, -1, -1): if (self.mZoomFactors[i] < self.mScale): self.setScale(self.mZoomFactors[i]) break def resetZoom(self): self.setScale(1) def comboActivated(self, index): self.setScale(self.mComboBox.itemData(index)) def comboEdited(self): pos = self.mComboRegExp.indexIn(self.mComboBox.currentText()) pos != -1 scale = qBound(self.mZoomFactors.first(), Float(self.mComboRegExp.cap(1)) / 100.0, self.mZoomFactors.last()) self.setScale(scale) def syncComboBox(self): if (not self.mComboBox): return index = self.mComboBox.findData(self.mScale) # For a custom scale, the current index must be set to -1 self.mComboBox.setCurrentIndex(index) self.mComboBox.setEditText(scaleToString(self.mScale))
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 Zoomable(QObject): scaleChanged = pyqtSignal(float) def __init__(self, parent = None): super().__init__(parent) self.mScale = 1 self.mZoomFactors = QVector() self.mGestureStartScale = 0 self.mComboBox = None self.mComboRegExp = QRegExp("^\\s*(\\d+)\\s*%?\\s*$") self.mComboValidator = None for i in range(zoomFactorCount): self.mZoomFactors.append(zoomFactors[i]) def setScale(self, scale): if (scale == self.mScale): return self.mScale = scale self.syncComboBox() self.scaleChanged.emit(self.mScale) def scale(self): return self.mScale def canZoomIn(self): return self.mScale < self.mZoomFactors.last() def canZoomOut(self): return self.mScale > self.mZoomFactors.first() ## # Changes the current scale based on the given mouse wheel \a delta. # # For convenience, the delta is assumed to be in the same units as # QWheelEvent.delta, which is the distance that the wheel is rotated, # in eighths of a degree. ## def handleWheelDelta(self, delta): if (delta <= -120): self.zoomOut() elif (delta >= 120): self.zoomIn() else: # We're dealing with a finer-resolution mouse. Allow it to have finer # control over the zoom level. factor = 1 + 0.3 * qAbs(delta / 8 / 15) if (delta < 0): factor = 1 / factor scale = qBound(self.mZoomFactors.first(), self.mScale * factor, self.mZoomFactors.back()) # Round to at most four digits after the decimal point self.setScale(math.floor(scale * 10000 + 0.5) / 10000) ## # Changes the current scale based on the given pinch gesture. ## def handlePinchGesture(self, pinch): if (not (pinch.changeFlags() & QPinchGesture.ScaleFactorChanged)): return x = pinch.state() if x==Qt.NoGesture: pass elif x==Qt.GestureStarted: self.mGestureStartScale = self.mScale # fall through elif x==Qt.GestureUpdated: factor = pinch.scaleFactor() scale = qBound(self.mZoomFactors.first(), self.mGestureStartScale * factor, self.mZoomFactors.back()) self.setScale(math.floor(scale * 10000 + 0.5) / 10000) elif x==Qt.GestureFinished: pass elif x==Qt.GestureCanceled: pass ## # Returns whether images should be smoothly transformed when drawn at the # current scale. This is the case when the scale is not 1 and smaller than # 2. ## def smoothTransform(self): return self.mScale != 1.0 and self.mScale < 2.0 def setZoomFactors(self, factors): self.mZoomFactors = factors def connectToComboBox(self, comboBox): if (self.mComboBox): self.mComboBox.disconnect() if (self.mComboBox.lineEdit()): self.mComboBox.lineEdit().disconnect() self.mComboBox.setValidator(None) self.mComboBox = comboBox if type(comboBox) is QComboBox: self.mComboBox.clear() for scale in self.mZoomFactors: self.mComboBox.addItem(scaleToString(scale), scale) self.syncComboBox() self.mComboBox.activated.connect(self.comboActivated) self.mComboBox.setEditable(True) self.mComboBox.setInsertPolicy(QComboBox.NoInsert) self.mComboBox.lineEdit().editingFinished.connect(self.comboEdited) if (not self.mComboValidator): self.mComboValidator = QRegExpValidator(self.mComboRegExp, self) self.mComboBox.setValidator(self.mComboValidator) def zoomIn(self): for scale in self.mZoomFactors: if (scale > self.mScale): self.setScale(scale) break def zoomOut(self): for i in range(self.mZoomFactors.count() - 1, -1, -1): if (self.mZoomFactors[i] < self.mScale): self.setScale(self.mZoomFactors[i]) break def resetZoom(self): self.setScale(1) def comboActivated(self, index): self.setScale(self.mComboBox.itemData(index)) def comboEdited(self): pos = self.mComboRegExp.indexIn(self.mComboBox.currentText()) pos != -1 scale = qBound(self.mZoomFactors.first(), Float(self.mComboRegExp.cap(1)) / 100.0, self.mZoomFactors.last()) self.setScale(scale) def syncComboBox(self): if (not self.mComboBox): return index = self.mComboBox.findData(self.mScale) # For a custom scale, the current index must be set to -1 self.mComboBox.setCurrentIndex(index) self.mComboBox.setEditText(scaleToString(self.mScale))
class TileStampManager(QObject): setStamp = pyqtSignal(TileStamp) def __init__(self, toolManager, parent = None): super().__init__(parent) self.mStampsByName = QMap() self.mQuickStamps = QVector() for i in range(TileStampManager.quickStampKeys().__len__()): self.mQuickStamps.append(0) self.mTileStampModel = TileStampModel(self) self.mToolManager = toolManager prefs = preferences.Preferences.instance() prefs.stampsDirectoryChanged.connect(self.stampsDirectoryChanged) self.mTileStampModel.stampAdded.connect(self.stampAdded) self.mTileStampModel.stampRenamed.connect(self.stampRenamed) self.mTileStampModel.stampChanged.connect(self.saveStamp) self.mTileStampModel.stampRemoved.connect(self.deleteStamp) self.loadStamps() def __del__(self): # needs to be over here where the TileStamp type is complete pass ## # Returns the keys used for quickly accessible tile stamps. # Note: To store a tile layer <Ctrl> is added. The given keys will work # for recalling the stored values. ## def quickStampKeys(): keys=[Qt.Key_1, Qt.Key_2, Qt.Key_3, Qt.Key_4, Qt.Key_5, Qt.Key_6, Qt.Key_7, Qt.Key_8, Qt.Key_9] return keys def tileStampModel(self): return self.mTileStampModel def createStamp(self): stamp = self.tampFromContext(self.mToolManager.selectedTool()) if (not stamp.isEmpty()): self.mTileStampModel.addStamp(stamp) return stamp def addVariation(self, targetStamp): stamp = stampFromContext(self.mToolManager.selectedTool()) if (stamp.isEmpty()): return if (stamp == targetStamp): # avoid easy mistake of adding duplicates return for variation in stamp.variations(): self.mTileStampModel.addVariation(targetStamp, variation) def selectQuickStamp(self, index): stamp = self.mQuickStamps.at(index) if (not stamp.isEmpty()): self.setStamp.emit(stamp) def createQuickStamp(self, index): stamp = stampFromContext(self.mToolManager.selectedTool()) if (stamp.isEmpty()): return self.setQuickStamp(index, stamp) def extendQuickStamp(self, index): quickStamp = self.mQuickStamps[index] if (quickStamp.isEmpty()): self.createQuickStamp(index) else: self.addVariation(quickStamp) def stampsDirectoryChanged(self): # erase current stamps self.mQuickStamps.fill(TileStamp()) self.mStampsByName.clear() self.mTileStampModel.clear() self.loadStamps() def eraseQuickStamp(self, index): stamp = self.mQuickStamps.at(index) if (not stamp.isEmpty()): self.mQuickStamps[index] = TileStamp() if (not self.mQuickStamps.contains(stamp)): self.mTileStampModel.removeStamp(stamp) def setQuickStamp(self, index, stamp): stamp.setQuickStampIndex(index) # make sure existing quickstamp is removed from stamp model self.eraseQuickStamp(index) self.mTileStampModel.addStamp(stamp) self.mQuickStamps[index] = stamp def loadStamps(self): prefs = preferences.Preferences.instance() stampsDirectory = prefs.stampsDirectory() stampsDir = QDir(stampsDirectory) iterator = QDirIterator(stampsDirectory, ["*.stamp"], QDir.Files | QDir.Readable) while (iterator.hasNext()): stampFileName = iterator.next() stampFile = QFile(stampFileName) if (not stampFile.open(QIODevice.ReadOnly)): continue data = stampFile.readAll() document = QJsonDocument.fromBinaryData(data) if (document.isNull()): # document not valid binary data, maybe it's an JSON text file error = QJsonParseError() document = QJsonDocument.fromJson(data, error) if (error.error != QJsonParseError.NoError): qDebug("Failed to parse stamp file:" + error.errorString()) continue stamp = TileStamp.fromJson(document.object(), stampsDir) if (stamp.isEmpty()): continue stamp.setFileName(iterator.fileInfo().fileName()) self.mTileStampModel.addStamp(stamp) index = stamp.quickStampIndex() if (index >= 0 and index < self.mQuickStamps.size()): self.mQuickStamps[index] = stamp def stampAdded(self, stamp): if (stamp.name().isEmpty() or self.mStampsByName.contains(stamp.name())): # pick the first available stamp name name = QString() index = self.mTileStampModel.stamps().size() while(self.mStampsByName.contains(name)): name = str(index) index += 1 stamp.setName(name) self.mStampsByName.insert(stamp.name(), stamp) if (stamp.fileName().isEmpty()): stamp.setFileName(findStampFileName(stamp.name())) self.saveStamp(stamp) def stampRenamed(self, stamp): existingName = self.mStampsByName.key(stamp) self.mStampsByName.remove(existingName) self.mStampsByName.insert(stamp.name(), stamp) existingFileName = stamp.fileName() newFileName = findStampFileName(stamp.name(), existingFileName) if (existingFileName != newFileName): if (QFile.rename(stampFilePath(existingFileName), stampFilePath(newFileName))): stamp.setFileName(newFileName) def saveStamp(self, stamp): # make sure we have a stamps directory prefs = preferences.Preferences.instance() stampsDirectory = prefs.stampsDirectory() stampsDir = QDir(stampsDirectory) if (not stampsDir.exists() and not stampsDir.mkpath(".")): qDebug("Failed to create stamps directory" + stampsDirectory) return filePath = stampsDir.filePath(stamp.fileName()) file = QSaveFile(filePath) if (not file.open(QIODevice.WriteOnly)): qDebug("Failed to open stamp file for writing" + filePath) return stampJson = stamp.toJson(QFileInfo(filePath).dir()) file.write(QJsonDocument(stampJson).toJson(QJsonDocument.Compact)) if (not file.commit()): qDebug() << "Failed to write stamp" << filePath def deleteStamp(self, stamp): self.mStampsByName.remove(stamp.name()) QFile.remove(stampFilePath(stamp.fileName()))
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()