def makeChunkVertices(self, chunk, limitBox): tilePositions = [] defaultColor = (0xff, 0xff, 0x33, 0x44) for i, ref in enumerate(chunk.TileEntities): if i % 10 == 0: yield if limitBox and ref.Position not in limitBox: continue if ref.id == "Control": continue tilePositions.append(ref.Position) if not len(tilePositions): return tiles = self._computeVertices(tilePositions, defaultColor, chunkPosition=chunk.chunkPosition) vertexNode = VertexNode([tiles]) polygonMode = PolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE) vertexNode.addState(polygonMode) lineWidth = LineWidth(2.0) vertexNode.addState(lineWidth) depthFunc = DepthFunc(GL.GL_ALWAYS) vertexNode.addState(depthFunc) self.sceneNode = Node("tileEntityLocations") self.sceneNode.addChild(vertexNode) vertexNode = VertexNode([tiles]) self.sceneNode.addChild(vertexNode)
def makeChunkVertices(self, chunk, limitBox): monsterPositions = [] for i, entityRef in enumerate(chunk.Entities): if i % 10 == 0: yield ID = entityRef.id if ID in self.notMonsters: continue pos = entityRef.Position if limitBox and pos not in limitBox: continue monsterPositions.append(pos) if not len(monsterPositions): return monsters = self._computeVertices(monsterPositions, (0xff, 0x22, 0x22, 0x44), offset=True, chunkPosition=chunk.chunkPosition) yield vertexNode = VertexNode(monsters) vertexNode.addState(PolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE)) vertexNode.addState(LineWidth(2.0)) vertexNode.addState(DepthFunc(GL.GL_ALWAYS)) self.sceneNode = Node("monsterLocations") self.sceneNode.addChild(vertexNode) vertexNode = VertexNode(monsters) self.sceneNode.addChild(vertexNode)
def __init__(self, dimension, textureAtlas=None, geometryCache=None, bounds=None): super(WorldScene, self).__init__() self.dimension = dimension self.textureAtlas = textureAtlas self.depthOffset = DepthOffset(DepthOffsets.Renderer) self.addState(self.depthOffset) self.textureAtlasNode = Node() self.textureAtlasState = TextureAtlasState(textureAtlas) self.textureAtlasNode.addState(self.textureAtlasState) self.addChild(self.textureAtlasNode) self.renderstateNodes = {} for rsClass in renderstates.allRenderstates: rsNode = Node() rsNode.addState(rsClass()) self.textureAtlasNode.addChild(rsNode) self.renderstateNodes[rsClass] = rsNode self.groupNodes = {} # by renderstate self.chunkRenderInfo = {} self.visibleLayers = set(Layer.DefaultVisibleLayers) self.updateTask = SceneUpdateTask(self, textureAtlas) if geometryCache is None: geometryCache = GeometryCache() self.geometryCache = geometryCache self.showRedraw = False self.minlod = 0 self.bounds = bounds
def createSceneGraph(self): sceneGraph = scenenode.Node("WorldView SceneGraph") self.worldScene = self.createWorldScene() self.worldScene.setVisibleLayers( self.layerToggleGroup.getVisibleLayers()) clearNode = ClearNode() self.skyNode = sky.SkyNode() self.loadableChunksNode = loadablechunks.LoadableChunksNode( self.dimension) self.worldNode = Node("World Container") self.matrixState = MatrixState() self.worldNode.addState(self.matrixState) self._updateMatrices() self.worldNode.addChild(self.loadableChunksNode) self.worldNode.addChild(self.worldScene) self.worldNode.addChild(self.overlayNode) sceneGraph.addChild(clearNode) sceneGraph.addChild(self.skyNode) sceneGraph.addChild(self.worldNode) sceneGraph.addChild(self.compassNode) if self.cursorNode: self.worldNode.addChild(self.cursorNode) return sceneGraph
def makeChunkVertices(self, chunk, limitBox): monsterPositions = [] for i, entityRef in enumerate(chunk.Entities): if i % 10 == 0: yield ID = entityRef.id if ID in self.notMonsters: continue pos = entityRef.Position if limitBox and pos not in limitBox: continue monsterPositions.append(pos) monsters = self._computeVertices(monsterPositions, (0xff, 0x22, 0x22, 0x44), offset=True, chunkPosition=chunk.chunkPosition) yield vertexNode = VertexNode(monsters) polyNode = PolygonModeNode(GL.GL_FRONT_AND_BACK, GL.GL_LINE) polyNode.addChild(vertexNode) lineNode = LineWidthNode(2.0) lineNode.addChild(polyNode) depthNode = DepthFuncNode(GL.GL_ALWAYS) depthNode.addChild(lineNode) self.sceneNode = Node() self.sceneNode.addChild(depthNode) vertexNode = VertexNode(monsters) self.sceneNode.addChild(vertexNode)
class TileEntityLocationMesh(EntityMeshBase): layer = Layer.TileEntityLocations def makeChunkVertices(self, chunk, limitBox): tilePositions = [] defaultColor = (0xff, 0xff, 0x33, 0x44) for i, ref in enumerate(chunk.TileEntities): if i % 10 == 0: yield if limitBox and ref.Position not in limitBox: continue if ref.id == "Control": continue tilePositions.append(ref.Position) tiles = self._computeVertices(tilePositions, defaultColor, chunkPosition=chunk.chunkPosition) vertexNode = VertexNode([tiles]) polyNode = PolygonModeNode(GL.GL_FRONT_AND_BACK, GL.GL_LINE) polyNode.addChild(vertexNode) lineNode = LineWidthNode(2.0) lineNode.addChild(polyNode) depthNode = DepthFuncNode(GL.GL_ALWAYS) depthNode.addChild(lineNode) self.sceneNode = Node() self.sceneNode.addChild(depthNode) vertexNode = VertexNode([tiles]) self.sceneNode.addChild(vertexNode)
class MonsterLocationRenderer(EntityMeshBase): layer = Layer.MonsterLocations notMonsters = {"Item", "XPOrb", "Painting", "ItemFrame"} def makeChunkVertices(self, chunk, limitBox): monsterPositions = [] for i, entityRef in enumerate(chunk.Entities): if i % 10 == 0: yield ID = entityRef.id if ID in self.notMonsters: continue pos = entityRef.Position if limitBox and pos not in limitBox: continue monsterPositions.append(pos) monsters = self._computeVertices(monsterPositions, (0xff, 0x22, 0x22, 0x44), offset=True, chunkPosition=chunk.chunkPosition) yield vertexNode = VertexNode(monsters) vertexNode.addState(PolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE)) vertexNode.addState(LineWidth(2.0)) vertexNode.addState(DepthFunc(GL.GL_ALWAYS)) self.sceneNode = Node() self.sceneNode.addChild(vertexNode) vertexNode = VertexNode(monsters) self.sceneNode.addChild(vertexNode)
def createSceneGraph(self): sceneGraph = scenenode.Node("WorldView SceneGraph") self.worldScene = self.createWorldScene() self.worldScene.setVisibleLayers(self.layerToggleGroup.getVisibleLayers()) clearNode = ClearNode() self.skyNode = sky.SkyNode() self.loadableChunksNode = loadablechunks.LoadableChunksNode(self.dimension) self.worldNode = Node("World Container") self.matrixState = MatrixState() self.worldNode.addState(self.matrixState) self._updateMatrices() self.worldNode.addChild(self.loadableChunksNode) self.worldNode.addChild(self.worldScene) self.worldNode.addChild(self.overlayNode) sceneGraph.addChild(clearNode) sceneGraph.addChild(self.skyNode) sceneGraph.addChild(self.worldNode) sceneGraph.addChild(self.compassNode) if self.cursorNode: self.worldNode.addChild(self.cursorNode) return sceneGraph
def __init__(self, editorSession, *args, **kwargs): super(BrushTool, self).__init__(editorSession, *args, **kwargs) self.toolWidget = BrushToolWidget() self.brushMode = None self.brushLoader = None self.brushModesByName = {cls.name:cls() for cls in BrushModeClasses} modes = self.brushModesByName.values() modes.sort(key=lambda m: m.name) self.toolWidget.brushModeInput.setModes(modes) BrushModeSetting.connectAndCall(self.modeSettingChanged) self.cursorWorldScene = None self.cursorNode = Node("brushCursor") self.cursorTranslate = Translate() self.cursorNode.addState(self.cursorTranslate) self.toolWidget.xSpinSlider.setMinimum(1) self.toolWidget.ySpinSlider.setMinimum(1) self.toolWidget.zSpinSlider.setMinimum(1) self.toolWidget.xSpinSlider.valueChanged.connect(self.setX) self.toolWidget.ySpinSlider.valueChanged.connect(self.setY) self.toolWidget.zSpinSlider.valueChanged.connect(self.setZ) self.toolWidget.brushShapeInput.shapeChanged.connect(self.updateCursor) self.toolWidget.brushShapeInput.shapeOptionsChanged.connect(self.updateCursor) self.fillBlock = editorSession.worldEditor.blocktypes["stone"] self.brushSize = BrushSizeSetting.value(QtGui.QVector3D(5, 5, 5)).toTuple() # calls updateCursor self.toolWidget.xSpinSlider.setValue(self.brushSize[0]) self.toolWidget.ySpinSlider.setValue(self.brushSize[1]) self.toolWidget.zSpinSlider.setValue(self.brushSize[2])
def CommandVisuals(pos, commandObj): visualCls = _visualClasses.get(commandObj.name) if visualCls is None: log.warn("No command found for %s", commandObj.name) return Node("nullCommandVisuals") else: return visualCls(pos, commandObj)
def makeChunkVertices(self, chunk, limitBox): tilePositions = [] tileColors = [] defaultColor = (0xff, 0x33, 0x33, 0x44) for i, ref in enumerate(chunk.TileEntities): if i % 10 == 0: yield if limitBox and ref.Position not in limitBox: continue if ref.id == "Control": tilePositions.append(ref.Position) cmdText = ref.Command if len(cmdText): if cmdText[0] == "/": cmdText = cmdText[1:] command, _ = cmdText.split(None, 1) color = commandColor(command) tileColors.append(color + (0x44,)) else: tileColors.append(defaultColor) else: continue tiles = self._computeVertices(tilePositions, tileColors, chunkPosition=chunk.chunkPosition) vertexNode = VertexNode([tiles]) vertexNode.addState(PolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE)) vertexNode.addState(LineWidth(2.0)) vertexNode.addState(DepthFunc(GL.GL_ALWAYS)) self.sceneNode = Node() self.sceneNode.addChild(vertexNode)
def __init__(self, editorSession, *args, **kwargs): super(BrushTool, self).__init__(editorSession, *args, **kwargs) self.toolWidget = BrushToolWidget() self.brushMode = None self.brushLoader = None self.brushModesByName = { cls.name: cls(self) for cls in BrushModeClasses } brushModes = self.brushModesByName.values() self.toolWidget.brushModeInput.setModes(brushModes) BrushModeSetting.connectAndCall(self.modeSettingChanged) self.cursorWorldScene = None self.cursorNode = Node("brushCursor") self.cursorTranslate = Translate() self.cursorNode.addState(self.cursorTranslate) self.toolWidget.xSpinSlider.setMinimum(1) self.toolWidget.ySpinSlider.setMinimum(1) self.toolWidget.zSpinSlider.setMinimum(1) self.toolWidget.xSpinSlider.valueChanged.connect(self.setX) self.toolWidget.ySpinSlider.valueChanged.connect(self.setY) self.toolWidget.zSpinSlider.valueChanged.connect(self.setZ) self.toolWidget.brushShapeInput.shapeChanged.connect(self.updateCursor) self.toolWidget.brushShapeInput.shapeOptionsChanged.connect( self.updateCursor) self.fillBlock = editorSession.worldEditor.blocktypes["stone"] self.brushSize = BrushSizeSetting.value(QtGui.QVector3D( 5, 5, 5)).toTuple() # calls updateCursor self.toolWidget.xSpinSlider.setValue(self.brushSize[0]) self.toolWidget.ySpinSlider.setValue(self.brushSize[1]) self.toolWidget.zSpinSlider.setValue(self.brushSize[2]) self.toolWidget.hoverSpinSlider.setValue(1) self.dragPoints = []
class CommandBlockLocationMesh(EntityMeshBase): layer = Layer.CommandBlockLocations def makeChunkVertices(self, chunk, limitBox): tilePositions = [] tileColors = [] defaultColor = (0xff, 0x33, 0x33, 0x44) for i, ref in enumerate(chunk.TileEntities): if i % 10 == 0: yield if limitBox and ref.Position not in limitBox: continue if ref.id == "Control": tilePositions.append(ref.Position) cmdText = ref.Command if len(cmdText): if cmdText[0] == "/": cmdText = cmdText[1:] command, _ = cmdText.split(None, 1) color = commandColor(command) tileColors.append(color + (0x44, )) else: tileColors.append(defaultColor) else: continue if not len(tileColors): return tiles = self._computeVertices(tilePositions, tileColors, chunkPosition=chunk.chunkPosition) vertexNode = VertexNode([tiles]) vertexNode.addState(PolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE)) vertexNode.addState(LineWidth(2.0)) vertexNode.addState(DepthFunc(GL.GL_ALWAYS)) self.sceneNode = Node("commandBlockLocations") self.sceneNode.addChild(vertexNode)
def makeChunkVertices(self, chunk, limitBox): tilePositions = [] tileColors = [] defaultColor = (0xff, 0x33, 0x33, 0x44) for i, ref in enumerate(chunk.TileEntities): if i % 10 == 0: yield if limitBox and ref.Position not in limitBox: continue if ref.id == "Control": tilePositions.append(ref.Position) cmdText = ref.Command if len(cmdText): if cmdText[0] == "/": cmdText = cmdText[1:] command, _ = cmdText.split(None, 1) color = commandColor(command) tileColors.append(color + (0x44, )) else: tileColors.append(defaultColor) else: continue tiles = self._computeVertices(tilePositions, tileColors, chunkPosition=chunk.chunkPosition) vertexNode = VertexNode([tiles]) polyNode = PolygonModeNode(GL.GL_FRONT_AND_BACK, GL.GL_LINE) polyNode.addChild(vertexNode) lineNode = LineWidthNode(2.0) lineNode.addChild(polyNode) depthNode = DepthFuncNode(GL.GL_ALWAYS) depthNode.addChild(lineNode) self.sceneNode = Node() self.sceneNode.addChild(depthNode)
def __init__(self, editorSession, *args, **kwargs): super(BrushTool, self).__init__(editorSession, *args, **kwargs) self.toolWidget = BrushToolWidget() self.brushMode = None self.brushLoader = None self.brushModesByName = {cls.name:cls(self) for cls in BrushModeClasses} brushModes = self.brushModesByName.values() self.toolWidget.brushModeInput.setModes(brushModes) BrushModeSetting.connectAndCall(self.modeSettingChanged) self.cursorWorldScene = None self.cursorBoxNode = None self.cursorNode = Node("brushCursor") self.cursorTranslate = Translate() self.cursorNode.addState(self.cursorTranslate) self.toolWidget.xSpinSlider.setMinimum(1) self.toolWidget.ySpinSlider.setMinimum(1) self.toolWidget.zSpinSlider.setMinimum(1) self.toolWidget.xSpinSlider.valueChanged.connect(self.setX) self.toolWidget.ySpinSlider.valueChanged.connect(self.setY) self.toolWidget.zSpinSlider.valueChanged.connect(self.setZ) self.toolWidget.brushShapeInput.shapeChanged.connect(self.updateCursor) self.toolWidget.brushShapeInput.shapeOptionsChanged.connect(self.updateCursor) self.brushSize = BrushSizeSetting.value(QtGui.QVector3D(5, 5, 5)).toTuple() # calls updateCursor self.toolWidget.xSpinSlider.setValue(self.brushSize[0]) self.toolWidget.ySpinSlider.setValue(self.brushSize[1]) self.toolWidget.zSpinSlider.setValue(self.brushSize[2]) self.toolWidget.hoverSpinSlider.setValue(1) self.dragPoints = []
def LineArcNode(p1, p2, color): arcSegments = 20 rgba = [c * 255 for c in color] points = [p1] x, y, z = p1 dx = p2[0] - p1[0] dz = p2[2] - p1[2] dx /= arcSegments dz /= arcSegments heightDiff = p2[1] - p1[1] # maxRise = 8 # initial y-velocity dy = 0.3 if heightDiff >= 0 else -0.3 dy += 2 * heightDiff / arcSegments # the height of p2 without gravity overshot = y + dy * arcSegments - p2[1] # needed gravity so the last point is p2 ddy = -overshot / (arcSegments * (arcSegments-1) / 2) for i in range(arcSegments): y += dy dy += ddy x += dx z += dz points.append((x, y, z)) arcNode = Node("lineArc") lineNode = LineStripNode(points, rgba) arcNode.addChild(lineNode) arcNode.addState(LineWidth(3.0)) backLineNode = Node("lineArcBack") backLineNode.addChild(lineNode) arcNode.addChild(backLineNode) backLineNode.addState(DepthFunc(GL.GL_GREATER)) backLineNode.addState(LineWidth(1.0)) return arcNode
def LineArcNode(p1, p2, color): arcSegments = 20 rgba = [c * 255 for c in color] points = [p1] x, y, z = p1 dx = p2[0] - p1[0] dz = p2[2] - p1[2] dx /= arcSegments dz /= arcSegments heightDiff = p2[1] - p1[1] # maxRise = 8 # initial y-velocity dy = 0.3 if heightDiff >= 0 else -0.3 dy += 2 * heightDiff / arcSegments # the height of p2 without gravity overshot = y + dy * arcSegments - p2[1] # needed gravity so the last point is p2 ddy = -overshot / (arcSegments * (arcSegments - 1) / 2) for i in range(arcSegments): y += dy dy += ddy x += dx z += dz points.append((x, y, z)) arcNode = Node("lineArc") lineNode = LineStripNode(points, rgba) arcNode.addChild(lineNode) arcNode.addState(LineWidth(3.0)) backLineNode = Node("lineArcBack") backLineNode.addChild(lineNode) arcNode.addChild(backLineNode) backLineNode.addState(DepthFunc(GL.GL_GREATER)) backLineNode.addState(LineWidth(1.0)) return arcNode
class WorldScene(scenenode.Node): def __init__(self, dimension, textureAtlas=None, geometryCache=None, bounds=None): super(WorldScene, self).__init__() self.dimension = dimension self.textureAtlas = textureAtlas self.depthOffset = DepthOffset(DepthOffsets.Renderer) self.addState(self.depthOffset) self.textureAtlasNode = Node() self.textureAtlasState = TextureAtlasState(textureAtlas) self.textureAtlasNode.addState(self.textureAtlasState) self.addChild(self.textureAtlasNode) self.renderstateNodes = {} for rsClass in renderstates.allRenderstates: rsNode = Node() rsNode.addState(rsClass()) self.textureAtlasNode.addChild(rsNode) self.renderstateNodes[rsClass] = rsNode self.groupNodes = {} # by renderstate self.chunkRenderInfo = {} self.visibleLayers = set(Layer.DefaultVisibleLayers) self.updateTask = SceneUpdateTask(self, textureAtlas) if geometryCache is None: geometryCache = GeometryCache() self.geometryCache = geometryCache self.showRedraw = False self.minlod = 0 self.bounds = bounds def setTextureAtlas(self, textureAtlas): if textureAtlas is not self.textureAtlas: self.textureAtlas = textureAtlas self.textureAtlasState.textureAtlas = textureAtlas self.updateTask.textureAtlas = textureAtlas self.discardAllChunks() def chunkPositions(self): return self.chunkRenderInfo.iterkeys() def getRenderstateGroup(self, rsClass): groupNode = self.groupNodes.get(rsClass) if groupNode is None: groupNode = ChunkGroupNode() self.groupNodes[rsClass] = groupNode self.renderstateNodes[rsClass].addChild(groupNode) return groupNode def discardChunk(self, cx, cz): """ Discard the chunk at the given position from the scene """ for groupNode in self.groupNodes.itervalues(): groupNode.discardChunkNode(cx, cz) self.chunkRenderInfo.pop((cx, cz), None) def discardChunks(self, chunks): for cx, cz in chunks: self.discardChunk(cx, cz) def discardAllChunks(self): for groupNode in self.groupNodes.itervalues(): groupNode.clear() self.chunkRenderInfo.clear() def invalidateChunk(self, cx, cz, invalidLayers=None): """ Mark the chunk for regenerating vertex data """ if invalidLayers is None: invalidLayers = Layer.AllLayers node = self.chunkRenderInfo.get((cx, cz)) if node: node.invalidLayers.update(invalidLayers) _fastLeaves = False @property def fastLeaves(self): return self._fastLeaves @fastLeaves.setter def fastLeaves(self, val): if self._fastLeaves != bool(val): self.discardAllChunks() self._fastLeaves = bool(val) _roughGraphics = False @property def roughGraphics(self): return self._roughGraphics @roughGraphics.setter def roughGraphics(self, val): if self._roughGraphics != bool(val): self.discardAllChunks() self._roughGraphics = bool(val) _showHiddenOres = False @property def showHiddenOres(self): return self._showHiddenOres @showHiddenOres.setter def showHiddenOres(self, val): if self._showHiddenOres != bool(val): self.discardAllChunks() self._showHiddenOres = bool(val) def wantsChunk(self, cPos): return self.updateTask.wantsChunk(cPos) def workOnChunk(self, chunk, visibleSections=None): return self.updateTask.workOnChunk(chunk, visibleSections) def chunkNotPresent(self, cPos): self.updateTask.chunkNotPresent(cPos) def getChunkRenderInfo(self, cPos): chunkInfo = self.chunkRenderInfo.get(cPos) if chunkInfo is None: #log.info("Creating ChunkRenderInfo %s in %s", cPos, self.worldScene) chunkInfo = ChunkRenderInfo(self, cPos) self.chunkRenderInfo[cPos] = chunkInfo return chunkInfo def setLayerVisible(self, layerName, visible): if visible: self.visibleLayers.add(layerName) else: self.visibleLayers.discard(layerName) for groupNode in self.groupNodes.itervalues(): groupNode.setLayerVisible(layerName, visible) def setVisibleLayers(self, layerNames): self.visibleLayers = set(layerNames)
class WorldView(QGLWidget): """ Superclass for the following views: IsoWorldView: Display the world using an isometric viewing angle, without perspective. Click and drag to pan the viewing area. Use the mouse wheel or a UI control to zoom. CameraWorldView: Display the world using a first-person viewing angle with perspective. Click and drag to pan the camera. Use WASD or click and drag to move the camera. CutawayWorldView: Display a single slice of the world. Click and drag to move sideways. Use the mouse wheel or a UI widget to move forward or backward. Use a UI widget to zoom. FourUpWorldView: Display up to four other world views at once. Default to three cutaways and one isometric view. """ viewportMoved = QtCore.Signal(QtGui.QWidget) cursorMoved = QtCore.Signal(QtGui.QMouseEvent) urlsDropped = QtCore.Signal(QtCore.QMimeData, Vector, faces.Face) mapItemDropped = QtCore.Signal(QtCore.QMimeData, Vector, faces.Face) mouseBlockPos = Vector(0, 0, 0) mouseBlockFace = faces.FaceYIncreasing doSwapBuffers = QtCore.Signal() def __init__(self, dimension, textureAtlas=None, geometryCache=None, sharedGLWidget=None): """ :param dimension: :type dimension: WorldEditorDimension :param textureAtlas: :type textureAtlas: TextureAtlas :param geometryCache: :type geometryCache: GeometryCache :param sharedGLWidget: :type sharedGLWidget: QGLWidget :return: :rtype: """ QGLWidget.__init__(self, shareWidget=sharedGLWidget) self.dimension = None self.worldScene = None self.loadableChunksNode = None self.textureAtlas = None validateWidgetQGLContext(self) self.bufferSwapDone = True if THREADED_BUFFER_SWAP: self.setAutoBufferSwap(False) self.bufferSwapThread = QtCore.QThread() self.bufferSwapper = BufferSwapper(self) self.bufferSwapper.moveToThread(self.bufferSwapThread) self.doSwapBuffers.connect(self.bufferSwapper.swap) self.bufferSwapThread.start() self.setAcceptDrops(True) self.setSizePolicy(QtGui.QSizePolicy.Policy.Expanding, QtGui.QSizePolicy.Policy.Expanding) self.setFocusPolicy(Qt.ClickFocus) self.layerToggleGroup = LayerToggleGroup() self.layerToggleGroup.layerToggled.connect(self.setLayerVisible) self.mouseRay = Ray(Vector(0, 1, 0), Vector(0, -1, 0)) self.setMouseTracking(True) self.lastAutoUpdate = time.time() self.autoUpdateInterval = 0.5 # frequency of screen redraws in response to loaded chunks self.compassNode = self.createCompass() self.compassOrtho = Ortho((1, float(self.height()) / self.width())) self.compassNode.addState(self.compassOrtho) self.viewActions = [] self.pressedKeys = set() self.setTextureAtlas(textureAtlas) if geometryCache is None and sharedGLWidget is not None: geometryCache = sharedGLWidget.geometryCache if geometryCache is None: geometryCache = GeometryCache() self.geometryCache = geometryCache self.worldNode = None self.skyNode = None self.overlayNode = scenenode.Node("WorldView Overlay") self.sceneGraph = None self.renderGraph = None self.frameSamples = deque(maxlen=500) self.frameSamples.append(time.time()) self.cursorNode = None self.setDimension(dimension) def waitForSwapThread(self): while not self.bufferSwapDone: QtGui.QApplication.processEvents( QtCore.QEventLoop.ExcludeUserInputEvents) def dealloc(self): log.info("Deallocating GL resources for worldview %s", self) if THREADED_BUFFER_SWAP: self.waitForSwapThread() self.bufferSwapThread.quit() self.makeCurrent() self.renderGraph.dealloc() def __str__(self): try: if self.dimension: dimName = displayName(self.dimension.worldEditor.filename ) + ": " + self.dimension.dimName else: dimName = "None" except Exception as e: return "%s trying to get node name" % e return "%s(%r)" % (self.__class__.__name__, dimName) # --- Displayed world --- def setDimension(self, dimension): """ :param dimension: :type dimension: WorldEditorDimension :return: :rtype: """ log.info("Changing %s to dimension %s", self, dimension) self.dimension = dimension self.waitForSwapThread() self.makeCurrent() if self.renderGraph: self.renderGraph.dealloc() self.sceneGraph = self.createSceneGraph() self.renderGraph = rendernode.createRenderNode(self.sceneGraph) self.resetLoadOrder() self.update() def setTextureAtlas(self, textureAtlas): self.textureAtlas = textureAtlas if self.worldScene: self.worldScene.setTextureAtlas(textureAtlas) if textureAtlas is not None: self.waitForSwapThread() self.makeCurrent() textureAtlas.load() self.resetLoadOrder() # --- Graph construction --- def createCompass(self): return compass.CompassNode() def createWorldScene(self): return worldscene.WorldScene(self.dimension, self.textureAtlas, self.geometryCache) def createSceneGraph(self): sceneGraph = scenenode.Node("WorldView SceneGraph") self.worldScene = self.createWorldScene() self.worldScene.setVisibleLayers( self.layerToggleGroup.getVisibleLayers()) clearNode = ClearNode() self.skyNode = sky.SkyNode() self.loadableChunksNode = loadablechunks.LoadableChunksNode( self.dimension) self.worldNode = Node("World Container") self.matrixState = MatrixState() self.worldNode.addState(self.matrixState) self._updateMatrices() self.worldNode.addChild(self.loadableChunksNode) self.worldNode.addChild(self.worldScene) self.worldNode.addChild(self.overlayNode) sceneGraph.addChild(clearNode) sceneGraph.addChild(self.skyNode) sceneGraph.addChild(self.worldNode) sceneGraph.addChild(self.compassNode) if self.cursorNode: self.worldNode.addChild(self.cursorNode) return sceneGraph # --- Tool support --- def setToolCursor(self, cursorNode): if self.cursorNode: self.worldNode.removeChild(self.cursorNode) self.cursorNode = cursorNode if cursorNode: self.worldNode.addChild(cursorNode) def setToolOverlays(self, overlayNodes): self.overlayNode.clear() for node in overlayNodes: self.overlayNode.addChild(node) # --- View settings --- def setLayerVisible(self, layerName, visible): self.worldScene.setLayerVisible(layerName, visible) self.resetLoadOrder() def setDayTime(self, value): if self.skyNode: self.skyNode.setDayTime(value) def _updateMatrices(self): self.updateMatrices() self.updateFrustum() #min = self.unprojectPoint(0, 0)[0] #max = self.unprojectPoint(self.width(), self.height())[0] #self.visibleBox = BoundingBox(min, (0, 0, 0)).union(BoundingBox(max, (0, 0, 0))) def updateMatrices(self): """ Subclasses must implement updateMatrices to set the projection and modelview matrices. Should set self.worldNode.projection and self.worldNode.modelview """ raise NotImplementedError def updateFrustum(self): matrix = self.matrixState.projection * self.matrixState.modelview self.frustum = Frustum.fromViewingMatrix(numpy.array(matrix.data())) def getViewCorners(self): """ Returns corners: bottom left, near bottom left, far top left, near top left, far bottom right, near bottom right, far top right, near top right, far :return: :rtype: list[QVector4D] """ corners = [ QtGui.QVector4D(x, y, z, 1.) for x, y, z in itertools.product((-1., 1.), (-1., 1.), (0., 1.)) ] matrix = self.matrixState.projection * self.matrixState.modelview matrix, inverted = matrix.inverted() worldCorners = [matrix.map(corner) for corner in corners] worldCorners = [ Vector(*((corner / corner.w()).toTuple()[:3])) for corner in worldCorners ] return worldCorners def getViewBounds(self): """ Get the corners of the viewing area, intersected with the world's bounds. xxx raycast to intersect with terrain height too :return: :rtype: """ corners = self.getViewCorners() # Convert the 4 corners into rays extending from the near point, then interpolate each ray at the # current dimension's height limits pairs = [] for i in range(0, 8, 2): pairs.append(corners[i:i + 2]) rays = [Ray.fromPoints(p1, p2) for p1, p2 in pairs] bounds = self.dimension.bounds pointPairs = [(r.atHeight(bounds.maxy), r.atHeight(bounds.miny)) for r in rays] return sum(pointPairs, ()) scaleChanged = QtCore.Signal(float) _scale = 1. / 16 @property def scale(self): return self._scale @scale.setter def scale(self, value): self._scale = value self._updateMatrices() log.debug("update(): scale %s %s", self, value) self.update() self.scaleChanged.emit(value) self.viewportMoved.emit(self) _centerPoint = Vector(0, 0, 0) @property def centerPoint(self): return self._centerPoint @centerPoint.setter def centerPoint(self, value): value = Vector(*value) if value != self._centerPoint: self._centerPoint = value self._updateMatrices() log.debug("update(): centerPoint %s %s", self, value) self.update() self.resetLoadOrder() self.viewportMoved.emit(self) def centerOnPoint(self, pos, distance=None): """Center the view on the given position""" # delta = self.viewCenter() - self.centerPoint # self.centerPoint = pos - delta self.centerPoint = pos self.update() def viewCenter(self): """Return the world position at the center of the view.""" #return self.unprojectAtHeight(self.width() / 2, self.height() / 2, 64.) # ray = self.rayAtPosition(self.width() / 2, self.height() / 2) # try: # point, face = raycast.rayCastInBounds(ray, self.dimension, 600) # except (raycast.MaxDistanceError, ValueError): # point = ray.atHeight(0) # return point or ray.point return self.centerPoint # --- Events --- def resizeEvent(self, event): center = self.viewCenter() self.compassOrtho.size = (1, float(self.height()) / self.width()) super(WorldView, self).resizeEvent(event) # log.info("WorldView: resized. moving to %s", center) # self.centerOnPoint(center) acceptableMimeTypes = [ MimeFormats.MapItem, ] def dragEnterEvent(self, event): # xxx show drop preview as scene node print("DRAG ENTER. FORMATS:\n%s" % event.mimeData().formats()) for mimeType in self.acceptableMimeTypes: if event.mimeData().hasFormat(mimeType): event.acceptProposedAction() return if event.mimeData().hasUrls(): event.acceptProposedAction() def dropEvent(self, event): mimeData = event.mimeData() x = event.pos().x() y = event.pos().y() ray = self.rayAtPosition(x, y) dropPosition, face = self.rayCastInView(ray) if mimeData.hasFormat(MimeFormats.MapItem): self.mapItemDropped.emit(mimeData, dropPosition, face) elif mimeData.hasUrls: self.urlsDropped.emit(mimeData, dropPosition, face) def keyPressEvent(self, event): self.augmentKeyEvent(event) self.pressedKeys.add(event.key()) for action in self.viewActions: if action.matchKeyEvent(event): action.keyPressEvent(event) def keyReleaseEvent(self, event): self.augmentKeyEvent(event) self.pressedKeys.discard(event.key()) for action in self.viewActions: if action.matchKeyEvent(event): action.keyReleaseEvent(event) def mousePressEvent(self, event): self.augmentMouseEvent(event) for action in self.viewActions: if action.button & event.button(): if action.matchModifiers(event): action.mousePressEvent(event) def mouseMoveEvent(self, event): self.augmentMouseEvent(event) for action in self.viewActions: if not action.button or action.button == event.buttons( ) or action.button & event.buttons(): # Important! mouseMove checks event.buttons(), press and release check event.button() if action.matchModifiers(event): if not action.key or action.key in self.pressedKeys: action.mouseMoveEvent(event) self.cursorMoved.emit(event) self.update() def mouseReleaseEvent(self, event): # Ignore modifiers on mouse release event and send mouse release to any # actions that are set to the given button. This handles this series of inputs, # for example: Control Key down, Mouse1 down, Control Key up, Mouse1 up self.augmentMouseEvent(event) for action in self.viewActions: if action.button & event.button(): action.mouseReleaseEvent(event) wheelPos = 0 def wheelEvent(self, event): self.augmentMouseEvent(event) for action in self.viewActions: if action.acceptsMouseWheel and ( (action.modifiers & event.modifiers()) or action.modifiers == event.modifiers()): self.wheelPos += event.delta() # event.delta reports eighths of a degree. a standard wheel tick is 15 degrees, or 120 eighths. # keep count of wheel position and emit an event for each 15 degrees turned. # xxx will we ever need sub-click precision for wheel events? clicks = 0 while self.wheelPos >= 120: self.wheelPos -= 120 clicks += 1 while self.wheelPos <= -120: self.wheelPos += 120 clicks -= 1 if action.button == action.WHEEL_UP and clicks > 0: for i in range(abs(clicks)): action.keyPressEvent(event) if action.button == action.WHEEL_DOWN and clicks < 0: for i in range(abs(clicks)): action.keyPressEvent(event) def augmentMouseEvent(self, event): x, y = event.x(), event.y() return self.augmentEvent(x, y, event) def augmentKeyEvent(self, event): globalPos = QtGui.QCursor.pos() mousePos = self.mapFromGlobal(globalPos) x = mousePos.x() y = mousePos.y() # xxx fake position of mouse event -- need to use mcedit internal event object already event.x = lambda: x event.y = lambda: y return self.augmentEvent(x, y, event) @profiler.function def augmentEvent(self, x, y, event): ray = self.rayAtPosition(x, y) event.ray = ray event.view = self position, face = self.rayCastInView(ray) self.mouseBlockPos = event.blockPosition = position self.mouseBlockFace = event.blockFace = face self.mouseRay = ray # --- OpenGL support --- def initializeGL(self, *args, **kwargs): GL.glEnableClientState(GL.GL_VERTEX_ARRAY) GL.glAlphaFunc(GL.GL_NOTEQUAL, 0) GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA) GL.glEnable(GL.GL_DEPTH_TEST) def resizeGL(self, width, height): GL.glViewport(0, 0, width, height) self._updateMatrices() maxFPS = 45 @profiler.function def glDraw(self, *args, **kwargs): if not self.bufferSwapDone: return frameInterval = 1.0 / self.maxFPS if time.time() - self.frameSamples[-1] < frameInterval: return super(WorldView, self).glDraw(*args, **kwargs) shouldRender = True def paintGL(self): if not self.shouldRender: return try: with profiler.context("paintGL: %s" % self): self.frameSamples.append(time.time()) if self.textureAtlas: self.textureAtlas.update() with profiler.context("renderScene"): rendernode.renderScene(self.renderGraph) if THREADED_BUFFER_SWAP: self.doneCurrent() self.bufferSwapDone = False self.doSwapBuffers.emit() except: self.shouldRender = False raise def swapDone(self): self.bufferSwapDone = True @property def fps(self): samples = 3 if len(self.frameSamples) <= samples: return 0.0 return (samples - 1) / (self.frameSamples[-1] - self.frameSamples[-samples]) # --- Screen<->world space conversion --- def rayCastInView(self, ray): try: result = raycast.rayCastInBounds(ray, self.dimension, maxDistance=200) position, face = result except (raycast.MaxDistanceError, ValueError): # GL.glReadBuffer(GL.GL_BACK) # pixel = GL.glReadPixels(x, self.height() - y, 1, 1, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT) # depth = -1 + 2 * pixel[0, 0] # p = self.pointsAtPositions((x, y, depth))[0] # # face = faces.FaceYIncreasing # position = p.intfloor() defaultDistance = 200 position = (ray.point + ray.vector * defaultDistance).intfloor() face = faces.FaceUp return position, face def rayAtPosition(self, x, y): """ Given coordinates in screen space, return a ray in 3D space. Parameters: x and y are coordinates local to this QWidget :rtype: Ray """ p0, p1 = self.pointsAtPositions((x, y, 0.0), (x, y, 1.0)) return Ray(p0, (p1 - p0).normalize()) def rayAtCenter(self): return self.rayAtPosition(self.width() / 2, self.height() / 2) def pointsAtPositions(self, *screenPoints): w = float(self.width()) h = float(self.height()) matrix = self.matrixState.projection * self.matrixState.modelview inverse, ok = matrix.inverted() if not ok or 0 in (w, h): return [Vector(0, 0, 0) for i in screenPoints] def _unproject(): for x, y, z in screenPoints: x = float(x) y = float(y) y = h - y x = 2 * x / w - 1 y = 2 * y / h - 1 def v(p): return Vector(p.x(), p.y(), p.z()) yield v(inverse.map(QtGui.QVector3D(x, y, z))) return list(_unproject()) def unprojectAtHeight(self, x, y, h): """ Given coordinates in screen space, find the corresponding point in 3D space. Like rayAtPosition, but the third parameter is a height value in 3D space. """ ray = self.rayAtPosition(x, y) return ray.atHeight(h) # --- Chunk loading --- _chunkIter = None def resetLoadOrder(self): self._chunkIter = None def makeChunkIter(self): x, y, z = self.viewCenter() return iterateChunks( x, z, 1 + max(self.width() * self.scale, self.height() * self.scale) // 32) def requestChunk(self): if self._chunkIter is None: self._chunkIter = self.makeChunkIter() try: for c in self._chunkIter: if self.worldScene.wantsChunk(c): return c except StopIteration: pass def wantsChunk(self, c): if not self.worldScene.wantsChunk(c): return False if hasattr(self, 'frustum'): point = [ c[0] * 16 + 8, self.dimension.bounds.miny + self.dimension.bounds.height / 2, c[1] * 16 + 8, 1.0 ] return self.frustum.visible1(point=point, radius=self.dimension.bounds.height / 2) return True def chunkNotPresent(self, cPos): self.worldScene.chunkNotPresent(cPos) def recieveChunk(self, chunk): t = time.time() if self.lastAutoUpdate + self.autoUpdateInterval < t: self.lastAutoUpdate = t log.debug("update(): receivedChunk %s %s", self, chunk) self.update() with profiler.getProfiler().context("preloadCulling"): if hasattr(self, 'frustum'): cx, cz = chunk.chunkPosition points = [(cx * 16 + 8, h + 8, cz * 16 + 8, 1.0) for h in chunk.sectionPositions()] points = numpy.array(points) visibleSections = self.frustum.visible(points, radius=8 * math.sqrt(2)) else: visibleSections = None return self.worldScene.workOnChunk(chunk, visibleSections) def chunkInvalid(self, (cx, cz), deleted): self.worldScene.invalidateChunk(cx, cz) if deleted: self.loadableChunksNode.dirty = True self.resetLoadOrder()
class BrushTool(EditorTool): name = "Brush" iconName = "brush" maxBrushSize = 512 def __init__(self, editorSession, *args, **kwargs): super(BrushTool, self).__init__(editorSession, *args, **kwargs) self.toolWidget = BrushToolWidget() self.brushMode = None self.brushLoader = None self.brushModesByName = { cls.name: cls(self) for cls in BrushModeClasses } brushModes = self.brushModesByName.values() self.toolWidget.brushModeInput.setModes(brushModes) BrushModeSetting.connectAndCall(self.modeSettingChanged) self.cursorWorldScene = None self.cursorNode = Node("brushCursor") self.cursorTranslate = Translate() self.cursorNode.addState(self.cursorTranslate) self.toolWidget.xSpinSlider.setMinimum(1) self.toolWidget.ySpinSlider.setMinimum(1) self.toolWidget.zSpinSlider.setMinimum(1) self.toolWidget.xSpinSlider.valueChanged.connect(self.setX) self.toolWidget.ySpinSlider.valueChanged.connect(self.setY) self.toolWidget.zSpinSlider.valueChanged.connect(self.setZ) self.toolWidget.brushShapeInput.shapeChanged.connect(self.updateCursor) self.toolWidget.brushShapeInput.shapeOptionsChanged.connect( self.updateCursor) self.fillBlock = editorSession.worldEditor.blocktypes["stone"] self.brushSize = BrushSizeSetting.value(QtGui.QVector3D( 5, 5, 5)).toTuple() # calls updateCursor self.toolWidget.xSpinSlider.setValue(self.brushSize[0]) self.toolWidget.ySpinSlider.setValue(self.brushSize[1]) self.toolWidget.zSpinSlider.setValue(self.brushSize[2]) self.toolWidget.hoverSpinSlider.setValue(1) self.dragPoints = [] @property def hoverDistance(self): return self.toolWidget.hoverSpinSlider.value() _brushSize = (0, 0, 0) @property def brushSize(self): return self._brushSize @brushSize.setter def brushSize(self, value): self._brushSize = value BrushSizeSetting.setValue(QtGui.QVector3D(*self.brushSize)) self.updateCursor() def setX(self, val): x, y, z = self.brushSize x = float(val) self.brushSize = x, y, z def setY(self, val): x, y, z = self.brushSize y = float(val) self.brushSize = x, y, z def setZ(self, val): x, y, z = self.brushSize z = float(val) self.brushSize = x, y, z def setBlocktypes(self, types): if len(types) == 0: return self.fillBlock = types[0] self.updateCursor() def hoverPosition(self, event): if event.blockPosition: vector = (event.blockFace.vector * self.hoverDistance) pos = event.blockPosition + vector return pos def mousePress(self, event): self.dragPoints[:] = [] pos = self.hoverPosition(event) if pos: self.dragPoints.append(pos) def mouseMove(self, event): pos = self.hoverPosition(event) if pos: self.cursorTranslate.translateOffset = pos def mouseDrag(self, event): p2 = self.hoverPosition(event) if p2: if len(self.dragPoints): p1 = self.dragPoints.pop(-1) points = list(bresenham.bresenham(p1, p2)) self.dragPoints.extend(points) else: self.dragPoints.append(p2) def mouseRelease(self, event): if not len(self.dragPoints): pos = self.hoverPosition(event) if pos: self.dragPoints.append(pos) if len(self.dragPoints): dragPoints = sorted(set(self.dragPoints)) self.dragPoints[:] = [] command = BrushCommand(self.editorSession, dragPoints, self.options) self.editorSession.pushCommand(command) @property def options(self): options = { 'brushSize': self.brushSize, 'brushShape': self.brushShape, 'brushMode': self.brushMode } options.update(self.brushMode.getOptions()) return options def modeSettingChanged(self, value): self.brushMode = self.brushModesByName[value] stack = self.toolWidget.modeOptionsStack while stack.count(): stack.removeWidget(stack.widget(0)) if self.brushMode.optionsWidget: stack.addWidget(self.brushMode.optionsWidget) @property def brushShape(self): return self.toolWidget.brushShapeInput.currentShape def updateCursor(self): log.info("Updating brush cursor") if self.cursorWorldScene: self.brushLoader.timer.stop() self.cursorNode.removeChild(self.cursorWorldScene) self.cursorNode.removeChild(self.cursorBoxNode) cursorLevel = self.brushMode.createCursorLevel(self) if cursorLevel is not None: self.cursorWorldScene = worldscene.WorldScene( cursorLevel, self.editorSession.textureAtlas) self.cursorWorldScene.depthOffset.depthOffset = DepthOffsets.PreviewRenderer self.cursorNode.addChild(self.cursorWorldScene) self.brushLoader = WorldLoader(self.cursorWorldScene) self.brushLoader.startLoader() cursorBox = self.brushMode.brushBoxForPoint((0, 0, 0), self.options) if cursorBox is not None: self.cursorBoxNode = SelectionBoxNode() self.cursorBoxNode.selectionBox = cursorBox self.cursorBoxNode.filled = False self.cursorNode.addChild(self.cursorBoxNode)
def __init__(self, *args, **kwargs): EditorTool.__init__(self, *args, **kwargs) self.livePreview = False self.blockPreview = False self.glPreview = True toolWidget = QtGui.QWidget() self.toolWidget = toolWidget column = [] self.generatorTypes = [pluginClass(self) for pluginClass in _pluginClasses] self.currentGenerator = None if len(self.generatorTypes): self.currentGenerator = self.generatorTypes[0] self.generatorTypeInput = QtGui.QComboBox() self.generatorTypesChanged() self.generatorTypeInput.currentIndexChanged.connect(self.currentTypeChanged) self.livePreviewCheckbox = QtGui.QCheckBox("Live Preview") self.livePreviewCheckbox.setChecked(self.livePreview) self.livePreviewCheckbox.toggled.connect(self.livePreviewToggled) self.blockPreviewCheckbox = QtGui.QCheckBox("Block Preview") self.blockPreviewCheckbox.setChecked(self.blockPreview) self.blockPreviewCheckbox.toggled.connect(self.blockPreviewToggled) self.glPreviewCheckbox = QtGui.QCheckBox("GL Preview") self.glPreviewCheckbox.setChecked(self.glPreview) self.glPreviewCheckbox.toggled.connect(self.glPreviewToggled) self.optionsHolder = QtGui.QStackedWidget() self.optionsHolder.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) self.generateButton = QtGui.QPushButton(self.tr("Generate")) self.generateButton.clicked.connect(self.generateClicked) column.append(self.generatorTypeInput) column.append(self.livePreviewCheckbox) column.append(self.blockPreviewCheckbox) column.append(self.glPreviewCheckbox) column.append(self.optionsHolder) column.append(self.generateButton) self.toolWidget.setLayout(Column(*column)) self.overlayNode = scenenode.Node() self.sceneHolderNode = Node() self.sceneTranslate = Translate() self.sceneHolderNode.addState(self.sceneTranslate) self.overlayNode.addChild(self.sceneHolderNode) self.previewNode = None self.boxHandleNode = BoxHandle() self.boxHandleNode.boundsChanged.connect(self.boundsDidChange) self.boxHandleNode.boundsChangedDone.connect(self.boundsDidChangeDone) self.overlayNode.addChild(self.boxHandleNode) self.worldScene = None self.loader = None self.previewBounds = None self.schematicBounds = None self.currentSchematic = None self.currentTypeChanged(0) # Name of last selected generator plugin is saved after unloading # so it can be reselected if it is immediately reloaded self._lastTypeName = None _GeneratePlugins.instance.pluginAdded.connect(self.addPlugin) _GeneratePlugins.instance.pluginRemoved.connect(self.removePlugin)
class PendingImportNode(Node, QtCore.QObject): __node_id_counter = 0 def __init__(self, pendingImport, textureAtlas, hasHandle=True): """ A scenegraph node displaying an object that will be imported later, including live and deferred views of the object with transformed items, and a BoxHandle for moving the item. Parameters ---------- pendingImport: PendingImport The object to be imported. The PendingImportNode responds to changes in this object's position, rotation, and scale. textureAtlas: TextureAtlas The textures and block models used to render the preview of the object. Attributes ---------- basePosition: Vector The pre-transform position of the pending import. This is equal to `self.pendingImport.basePosition` except when the node is currently being dragged. transformedPosition: Vector The post-transform position of the pending import. This is equal to `self.pendingImport.importPos` except when the node is currently being dragged, scaled, or rotated. """ super(PendingImportNode, self).__init__() self.textureAtlas = textureAtlas self.pendingImport = pendingImport self.hasHandle = hasHandle dim = pendingImport.sourceDim self.transformedPosition = Vector(0, 0, 0) # worldScene is contained by rotateNode, and # translates the world scene back to 0, 0, 0 so the rotateNode can # rotate it around the anchor, and the plainSceneNode can translate # it to the current position. self.worldScene = WorldScene(dim, textureAtlas, bounds=pendingImport.selection) self.worldScene.depthOffset.depthOffset = DepthOffsets.PreviewRenderer self.worldSceneTranslate = Translate(-self.pendingImport.selection.origin) self.worldScene.addState(self.worldSceneTranslate) # rotateNode is used to rotate the non-transformed preview during live rotation self.rotateNode = Rotate3DNode() self.rotateNode.setAnchor(self.pendingImport.selection.size * 0.5) self.rotateNode.addChild(self.worldScene) # plainSceneNode contains the non-transformed preview of the imported # object, including its world scene. This preview will be rotated model-wise # while the user is dragging the rotate controls. self.plainSceneNode = Node("plainScene") self.positionTranslate = Translate() self.plainSceneNode.addState(self.positionTranslate) self.plainSceneNode.addChild(self.rotateNode) self.addChild(self.plainSceneNode) # transformedSceneNode contains the transformed preview of the imported # object, including a world scene that displays the object wrapped by a # DimensionTransform. self.transformedSceneNode = Node("transformedScene") self.transformedSceneTranslate = Translate() self.transformedSceneNode.addState(self.transformedSceneTranslate) self.transformedWorldScene = None self.addChild(self.transformedSceneNode) box = BoundingBox(pendingImport.importPos, pendingImport.importBounds.size) if hasHandle: # handleNode displays a bounding box that can be moved around, and responds # to mouse events. self.handleNode = BoxHandle() self.handleNode.bounds = box self.handleNode.resizable = False self.boxNode = None else: # boxNode displays a plain, non-movable bounding box self.boxNode = SelectionBoxNode() self.boxNode.wireColor = (1, 1, 1, .2) self.boxNode.filled = False self.handleNode = None self.addChild(self.boxNode) self.updateTransformedScene() self.basePosition = pendingImport.basePosition if hasHandle: self.handleNode.boundsChanged.connect(self.handleBoundsChanged) self.handleNode.boundsChangedDone.connect(self.handleBoundsChangedDone) self.addChild(self.handleNode) # loads the non-transformed world scene asynchronously. self.loader = WorldLoader(self.worldScene, list(pendingImport.selection.chunkPositions())) self.loader.startLoader(0.1 if self.hasHandle else 0.0) self.pendingImport.positionChanged.connect(self.setPosition) self.pendingImport.rotationChanged.connect(self.setRotation) # Emitted when the user finishes dragging the box handle and releases the mouse # button. Arguments are (newPosition, oldPosition). importMoved = QtCore.Signal(object, object) # Emitted while the user is dragging the box handle. Argument is the box origin. importIsMoving = QtCore.Signal(object) def handleBoundsChangedDone(self, bounds, oldBounds): point = self.getBaseFromTransformed(bounds.origin) oldPoint = self.getBaseFromTransformed(oldBounds.origin) if point != oldPoint: self.importMoved.emit(point, oldPoint) def handleBoundsChanged(self, bounds): self.setPreviewBasePosition(bounds.origin) def setPreviewBasePosition(self, origin): point = self.getBaseFromTransformed(origin) if self.basePosition != point: self.basePosition = point self.importIsMoving.emit(point) def getBaseFromTransformed(self, point): return point - self.pendingImport.transformOffset def setPreviewRotation(self, rots): self.plainSceneNode.visible = True self.transformedSceneNode.visible = False self.rotateNode.setRotation(rots) def setRotation(self, rots): self.updateTransformedScene() self.updateBoxHandle() self.rotateNode.setRotation(rots) def updateTransformedScene(self): if self.pendingImport.transformedDim is not None: log.info("Showing transformed scene") self.plainSceneNode.visible = False self.transformedSceneNode.visible = True if self.transformedWorldScene: self.transformedSceneNode.removeChild(self.transformedWorldScene) self.transformedWorldScene = WorldScene(self.pendingImport.transformedDim, self.textureAtlas) self.transformedWorldScene.depthOffset.depthOffset = DepthOffsets.PreviewRenderer self.transformedSceneNode.addChild(self.transformedWorldScene) self.updateTransformedPosition() cPos = list(self.pendingImport.transformedDim.chunkPositions()) self.loader = WorldLoader(self.transformedWorldScene, cPos) # ALERT!: self.hasHandle is overloaded with the meaning: # "not the first clone in a repeated clone" self.loader.startLoader(0.1 if self.hasHandle else 0.0) else: log.info("Hiding transformed scene") self.plainSceneNode.visible = True self.transformedSceneNode.visible = False if self.transformedWorldScene: self.transformedSceneNode.removeChild(self.transformedWorldScene) self.transformedWorldScene = None def updateTransformedPosition(self): self.transformedPosition = self.basePosition + self.pendingImport.transformOffset self.transformedSceneTranslate.translateOffset = self.transformedPosition - self.pendingImport.importDim.bounds.origin def updateBoxHandle(self): if self.transformedWorldScene is None: bounds = BoundingBox(self.basePosition, self.pendingImport.bounds.size) else: origin = self.transformedPosition bounds = BoundingBox(origin, self.pendingImport.importBounds.size) #if self.handleNode.bounds.size != bounds.size: if self.hasHandle: self.handleNode.bounds = bounds else: self.boxNode.selectionBox = bounds @property def basePosition(self): return self.positionTranslate.translateOffset @basePosition.setter def basePosition(self, value): value = Vector(*value) if value == self.positionTranslate.translateOffset: return self.positionTranslate.translateOffset = value self.updateTransformedPosition() self.updateBoxHandle() def setPosition(self, pos): self.basePosition = pos # --- Mouse events --- # inherit from BoxHandle? def mouseMove(self, event): if not self.hasHandle: return self.handleNode.mouseMove(event) def mousePress(self, event): if not self.hasHandle: return self.handleNode.mousePress(event) def mouseRelease(self, event): if not self.hasHandle: return self.handleNode.mouseRelease(event)
class GenerateTool(EditorTool): name = "Generate" iconName = "generate" instantDisplayChunks = 32 modifiesWorld = True def __init__(self, *args, **kwargs): EditorTool.__init__(self, *args, **kwargs) self.livePreview = False self.blockPreview = False self.glPreview = True toolWidget = QtGui.QWidget() self.toolWidget = toolWidget column = [] self.generatorTypes = [ pluginClass(self) for pluginClass in GeneratePlugins.registeredPlugins ] self.currentGenerator = None if len(self.generatorTypes): self.currentGenerator = self.generatorTypes[0] self.generatorTypeInput = QtGui.QComboBox() self.generatorTypesChanged() self.generatorTypeInput.currentIndexChanged.connect( self.currentTypeChanged) self.livePreviewCheckbox = QtGui.QCheckBox("Live Preview") self.livePreviewCheckbox.setChecked(self.livePreview) self.livePreviewCheckbox.toggled.connect(self.livePreviewToggled) self.blockPreviewCheckbox = QtGui.QCheckBox("Block Preview") self.blockPreviewCheckbox.setChecked(self.blockPreview) self.blockPreviewCheckbox.toggled.connect(self.blockPreviewToggled) self.glPreviewCheckbox = QtGui.QCheckBox("GL Preview") self.glPreviewCheckbox.setChecked(self.glPreview) self.glPreviewCheckbox.toggled.connect(self.glPreviewToggled) self.optionsHolder = QtGui.QStackedWidget() self.optionsHolder.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) self.generateButton = QtGui.QPushButton(self.tr("Generate")) self.generateButton.clicked.connect(self.generateClicked) column.append(self.generatorTypeInput) column.append(self.livePreviewCheckbox) column.append(self.blockPreviewCheckbox) column.append(self.glPreviewCheckbox) column.append(self.optionsHolder) column.append(self.generateButton) self.toolWidget.setLayout(Column(*column)) self.overlayNode = scenenode.Node("generateOverlay") self.sceneHolderNode = Node("sceneHolder") self.sceneTranslate = Translate() self.sceneHolderNode.addState(self.sceneTranslate) self.overlayNode.addChild(self.sceneHolderNode) self.previewNode = None self.boxHandleNode = BoxHandle() self.boxHandleNode.boundsChanged.connect(self.boundsDidChange) self.boxHandleNode.boundsChangedDone.connect(self.boundsDidChangeDone) self.overlayNode.addChild(self.boxHandleNode) self.worldScene = None self.loader = None self.previewBounds = None self.schematicBounds = None self.currentSchematic = None self.currentTypeChanged(0) # Name of last selected generator plugin is saved after unloading # so it can be reselected if it is immediately reloaded self._lastTypeName = None GeneratePlugins.pluginAdded.connect(self.addPlugin) GeneratePlugins.pluginRemoved.connect(self.removePlugin) def removePlugin(self, cls): log.info("Removing plugin %s", cls.__name__) self.generatorTypes[:] = [ gt for gt in self.generatorTypes if not isinstance(gt, cls) ] self.generatorTypesChanged() if self.currentGenerator not in self.generatorTypes: lastTypeName = self.currentGenerator.__class__.__name__ self.currentTypeChanged(0) # resets self._lastTypeName self._lastTypeName = lastTypeName def addPlugin(self, cls): log.info("Adding plugin %s", cls.__name__) self.generatorTypes.append(cls(self)) self.generatorTypesChanged() if self._lastTypeName is not None: if cls.__name__ == self._lastTypeName: self.currentTypeChanged(len(self.generatorTypes) - 1) def generatorTypesChanged(self): self.generatorTypeInput.clear() for gt in self.generatorTypes: if hasattr(gt, 'displayName'): displayName = gt.displayName else: displayName = gt.__class__.__name__ self.generatorTypeInput.addItem(displayName, gt) def livePreviewToggled(self, value): self.livePreview = value def blockPreviewToggled(self, value): self.blockPreview = value if value: if self.currentSchematic: self.displaySchematic(self.currentSchematic, self.schematicBounds.origin) else: self.updateBlockPreview() else: self.clearSchematic() def glPreviewToggled(self, value): self.glPreview = value if value: self.updateNodePreview() # xxx cache previewNode? else: self.clearNode() def currentTypeChanged(self, index): # user selected generator type after old type was unloaded, so forget the old type self._lastTypeName = None self.optionsHolder.removeWidget(self.optionsHolder.widget(0)) if index < len(self.generatorTypes): self.currentGenerator = self.generatorTypes[index] self.optionsHolder.addWidget( self.currentGenerator.getOptionsWidget()) self.updatePreview() else: self.currentGenerator = None self.clearSchematic() self.clearNode() log.info("Chose generator %s", repr(self.currentGenerator)) def mousePress(self, event): self.boxHandleNode.mousePress(event) def mouseMove(self, event): self.boxHandleNode.mouseMove(event) def mouseRelease(self, event): self.boxHandleNode.mouseRelease(event) def boundsDidChange(self, bounds): # box still being resized if not self.currentGenerator: return if not self.livePreview: return self.previewBounds = bounds self.currentGenerator.boundsChanged(bounds) self.updatePreview() def boundsDidChangeDone(self, bounds, oldBounds): # box finished resize if not self.currentGenerator: return self.previewBounds = bounds self.schematicBounds = bounds self.currentGenerator.boundsChanged(bounds) self.updatePreview() def clearNode(self): if self.previewNode: self.overlayNode.removeChild(self.previewNode) self.previewNode = None def updatePreview(self): if self.blockPreview: self.updateBlockPreview() else: self.clearSchematic() if self.glPreview: self.updateNodePreview() else: self.clearNode() def updateBlockPreview(self): bounds = self.previewBounds if bounds is not None and bounds.volume > 0: self.generateNextSchematic(bounds) else: self.clearSchematic() def updateNodePreview(self): bounds = self.previewBounds if self.currentGenerator is None: return if bounds is not None and bounds.volume > 0: try: node = self.currentGenerator.getPreviewNode(bounds) except Exception: log.exception( "Error while getting scene nodes from generator:") else: if node is not None: self.clearNode() if isinstance(node, list): nodes = node node = scenenode.Node("generatePreviewHolder") for c in nodes: node.addChild(c) self.overlayNode.addChild(node) self.previewNode = node def generateNextSchematic(self, bounds): if bounds is None: self.clearSchematic() return if self.currentGenerator is None: return try: schematic = self.currentGenerator.generate( bounds, self.editorSession.worldEditor.blocktypes) self.currentSchematic = schematic self.displaySchematic(schematic, bounds.origin) except Exception as e: log.exception("Error while running generator %s: %s", self.currentGenerator, e) QtGui.QMessageBox.warning( QtGui.qApp.mainWindow, "Error while running generator", "An error occurred while running the generator: \n %s.\n\n" "Traceback: %s" % (e, traceback.format_exc())) self.livePreview = False def displaySchematic(self, schematic, offset): if schematic is not None: dim = schematic.getDimension() if self.worldScene: self.sceneHolderNode.removeChild(self.worldScene) self.loader.timer.stop() self.loader = None atlas = self.editorSession.textureAtlas self.worldScene = WorldScene(dim, atlas) self.sceneTranslate.translateOffset = offset self.sceneHolderNode.addChild(self.worldScene) self.loader = WorldLoader(self.worldScene) if dim.chunkCount() <= self.instantDisplayChunks: exhaust(self.loader.work()) else: self.loader.startLoader() else: self.clearSchematic() def clearSchematic(self): if self.worldScene: self.sceneHolderNode.removeChild(self.worldScene) self.worldScene = None self.loader.timer.stop() self.loader = None def generateClicked(self): if self.currentGenerator is None: return if self.schematicBounds is None: log.info("schematicBounds is None, not generating") return if self.currentSchematic is None: log.info("Generating new schematic for import") currentSchematic = self.currentGenerator.generate( self.schematicBounds, self.editorSession.worldEditor.blocktypes) else: log.info("Importing previously generated schematic.") currentSchematic = self.currentSchematic command = GenerateCommand(self, self.schematicBounds) try: with command.begin(): if currentSchematic is not None: task = self.editorSession.currentDimension.importSchematicIter( currentSchematic, self.schematicBounds.origin) showProgress(self.tr("Importing generated object..."), task) else: task = self.currentGenerator.generateInWorld( self.schematicBounds, self.editorSession.currentDimension) showProgress(self.tr("Generating object in world..."), task) except Exception as e: log.exception("Error while importing or generating in world: %r" % e) command.undo() else: self.editorSession.pushCommand(command)
class WorldView(QGLWidget): """ Superclass for the following views: IsoWorldView: Display the world using an isometric viewing angle, without perspective. Click and drag to pan the viewing area. Use the mouse wheel or a UI control to zoom. CameraWorldView: Display the world using a first-person viewing angle with perspective. Click and drag to pan the camera. Use WASD or click and drag to move the camera. CutawayWorldView: Display a single slice of the world. Click and drag to move sideways. Use the mouse wheel or a UI widget to move forward or backward. Use a UI widget to zoom. FourUpWorldView: Display up to four other world views at once. Default to three cutaways and one isometric view. """ viewportMoved = QtCore.Signal(QtGui.QWidget) cursorMoved = QtCore.Signal(QtGui.QMouseEvent) urlsDropped = QtCore.Signal(QtCore.QMimeData, Vector, faces.Face) mapItemDropped = QtCore.Signal(QtCore.QMimeData, Vector, faces.Face) mouseBlockPos = Vector(0, 0, 0) mouseBlockFace = faces.FaceYIncreasing doSwapBuffers = QtCore.Signal() def __init__(self, dimension, textureAtlas=None, geometryCache=None, sharedGLWidget=None): """ :param dimension: :type dimension: WorldEditorDimension :param textureAtlas: :type textureAtlas: TextureAtlas :param geometryCache: :type geometryCache: GeometryCache :param sharedGLWidget: :type sharedGLWidget: QGLWidget :return: :rtype: """ QGLWidget.__init__(self, shareWidget=sharedGLWidget) self.dimension = None self.worldScene = None self.loadableChunksNode = None self.textureAtlas = None validateWidgetQGLContext(self) self.bufferSwapDone = True if THREADED_BUFFER_SWAP: self.setAutoBufferSwap(False) self.bufferSwapThread = QtCore.QThread() self.bufferSwapper = BufferSwapper(self) self.bufferSwapper.moveToThread(self.bufferSwapThread) self.doSwapBuffers.connect(self.bufferSwapper.swap) self.bufferSwapThread.start() self.setAcceptDrops(True) self.setSizePolicy(QtGui.QSizePolicy.Policy.Expanding, QtGui.QSizePolicy.Policy.Expanding) self.setFocusPolicy(Qt.ClickFocus) self.layerToggleGroup = LayerToggleGroup() self.layerToggleGroup.layerToggled.connect(self.setLayerVisible) self.mouseRay = Ray(Vector(0, 1, 0), Vector(0, -1, 0)) self.setMouseTracking(True) self.lastAutoUpdate = time.time() self.autoUpdateInterval = 0.5 # frequency of screen redraws in response to loaded chunks self.compassNode = self.createCompass() self.compassOrtho = Ortho((1, float(self.height()) / self.width())) self.compassNode.addState(self.compassOrtho) self.viewActions = [] self.pressedKeys = set() self.setTextureAtlas(textureAtlas) if geometryCache is None and sharedGLWidget is not None: geometryCache = sharedGLWidget.geometryCache if geometryCache is None: geometryCache = GeometryCache() self.geometryCache = geometryCache self.worldNode = None self.skyNode = None self.overlayNode = scenenode.Node("WorldView Overlay") self.sceneGraph = None self.renderGraph = None self.frameSamples = deque(maxlen=500) self.frameSamples.append(time.time()) self.cursorNode = None self.setDimension(dimension) def waitForSwapThread(self): while not self.bufferSwapDone: QtGui.QApplication.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) def dealloc(self): log.info("Deallocating GL resources for worldview %s", self) if THREADED_BUFFER_SWAP: self.waitForSwapThread() self.bufferSwapThread.quit() self.makeCurrent() self.renderGraph.dealloc() def __str__(self): try: if self.dimension: dimName = displayName(self.dimension.worldEditor.filename) + ": " + self.dimension.dimName else: dimName = "None" except Exception as e: return "%s trying to get node name" % e return "%s(%r)" % (self.__class__.__name__, dimName) # --- Displayed world --- def setDimension(self, dimension): """ :param dimension: :type dimension: WorldEditorDimension :return: :rtype: """ log.info("Changing %s to dimension %s", self, dimension) self.dimension = dimension self.waitForSwapThread() self.makeCurrent() if self.renderGraph: self.renderGraph.dealloc() self.sceneGraph = self.createSceneGraph() self.renderGraph = rendernode.createRenderNode(self.sceneGraph) self.resetLoadOrder() self.update() def setTextureAtlas(self, textureAtlas): self.textureAtlas = textureAtlas if self.worldScene: self.worldScene.setTextureAtlas(textureAtlas) if textureAtlas is not None: self.waitForSwapThread() self.makeCurrent() textureAtlas.load() self.resetLoadOrder() # --- Graph construction --- def createCompass(self): return compass.CompassNode() def createWorldScene(self): return worldscene.WorldScene(self.dimension, self.textureAtlas, self.geometryCache) def createSceneGraph(self): sceneGraph = scenenode.Node("WorldView SceneGraph") self.worldScene = self.createWorldScene() self.worldScene.setVisibleLayers(self.layerToggleGroup.getVisibleLayers()) clearNode = ClearNode() self.skyNode = sky.SkyNode() self.loadableChunksNode = loadablechunks.LoadableChunksNode(self.dimension) self.worldNode = Node("World Container") self.matrixState = MatrixState() self.worldNode.addState(self.matrixState) self._updateMatrices() self.worldNode.addChild(self.loadableChunksNode) self.worldNode.addChild(self.worldScene) self.worldNode.addChild(self.overlayNode) sceneGraph.addChild(clearNode) sceneGraph.addChild(self.skyNode) sceneGraph.addChild(self.worldNode) sceneGraph.addChild(self.compassNode) if self.cursorNode: self.worldNode.addChild(self.cursorNode) return sceneGraph # --- Tool support --- def setToolCursor(self, cursorNode): if self.cursorNode: self.worldNode.removeChild(self.cursorNode) self.cursorNode = cursorNode if cursorNode: self.worldNode.addChild(cursorNode) def setToolOverlays(self, overlayNodes): self.overlayNode.clear() for node in overlayNodes: self.overlayNode.addChild(node) # --- View settings --- def setLayerVisible(self, layerName, visible): self.worldScene.setLayerVisible(layerName, visible) self.resetLoadOrder() def setDayTime(self, value): if self.skyNode: self.skyNode.setDayTime(value) def _updateMatrices(self): self.updateMatrices() self.updateFrustum() #min = self.unprojectPoint(0, 0)[0] #max = self.unprojectPoint(self.width(), self.height())[0] #self.visibleBox = BoundingBox(min, (0, 0, 0)).union(BoundingBox(max, (0, 0, 0))) def updateMatrices(self): """ Subclasses must implement updateMatrices to set the projection and modelview matrices. Should set self.worldNode.projection and self.worldNode.modelview """ raise NotImplementedError def updateFrustum(self): matrix = self.matrixState.projection * self.matrixState.modelview self.frustum = Frustum.fromViewingMatrix(numpy.array(matrix.data())) def getViewCorners(self): """ Returns corners: bottom left, near bottom left, far top left, near top left, far bottom right, near bottom right, far top right, near top right, far :return: :rtype: list[QVector4D] """ corners = [QtGui.QVector4D(x, y, z, 1.) for x, y, z in itertools.product((-1., 1.), (-1., 1.), (0., 1.))] matrix = self.matrixState.projection * self.matrixState.modelview matrix, inverted = matrix.inverted() worldCorners = [matrix.map(corner) for corner in corners] worldCorners = [Vector(*((corner / corner.w()).toTuple()[:3])) for corner in worldCorners] return worldCorners def getViewBounds(self): """ Get the corners of the viewing area, intersected with the world's bounds. xxx raycast to intersect with terrain height too :return: :rtype: """ corners = self.getViewCorners() # Convert the 4 corners into rays extending from the near point, then interpolate each ray at the # current dimension's height limits pairs = [] for i in range(0, 8, 2): pairs.append(corners[i:i+2]) rays = [Ray.fromPoints(p1, p2) for p1, p2 in pairs] bounds = self.dimension.bounds pointPairs = [(r.atHeight(bounds.maxy), r.atHeight(bounds.miny)) for r in rays] return sum(pointPairs, ()) scaleChanged = QtCore.Signal(float) _scale = 1. / 16 @property def scale(self): return self._scale @scale.setter def scale(self, value): self._scale = value self._updateMatrices() log.debug("update(): scale %s %s", self, value) self.update() self.scaleChanged.emit(value) self.viewportMoved.emit(self) _centerPoint = Vector(0, 0, 0) @property def centerPoint(self): return self._centerPoint @centerPoint.setter def centerPoint(self, value): value = Vector(*value) if value != self._centerPoint: self._centerPoint = value self._updateMatrices() log.debug("update(): centerPoint %s %s", self, value) self.update() self.resetLoadOrder() self.viewportMoved.emit(self) def centerOnPoint(self, pos, distance=None): """Center the view on the given position""" # delta = self.viewCenter() - self.centerPoint # self.centerPoint = pos - delta self.centerPoint = pos self.update() def viewCenter(self): """Return the world position at the center of the view.""" #return self.unprojectAtHeight(self.width() / 2, self.height() / 2, 64.) # ray = self.rayAtPosition(self.width() / 2, self.height() / 2) # try: # point, face = raycast.rayCastInBounds(ray, self.dimension, 600) # except (raycast.MaxDistanceError, ValueError): # point = ray.atHeight(0) # return point or ray.point return self.centerPoint # --- Events --- def resizeEvent(self, event): center = self.viewCenter() self.compassOrtho.size = (1, float(self.height()) / self.width()) super(WorldView, self).resizeEvent(event) # log.info("WorldView: resized. moving to %s", center) # self.centerOnPoint(center) acceptableMimeTypes = [ MimeFormats.MapItem, ] def dragEnterEvent(self, event): # xxx show drop preview as scene node print("DRAG ENTER. FORMATS:\n%s" % event.mimeData().formats()) for mimeType in self.acceptableMimeTypes: if event.mimeData().hasFormat(mimeType): event.acceptProposedAction() return if event.mimeData().hasUrls(): event.acceptProposedAction() def dropEvent(self, event): mimeData = event.mimeData() x = event.pos().x() y = event.pos().y() ray = self.rayAtPosition(x, y) dropPosition, face = self.rayCastInView(ray) if mimeData.hasFormat(MimeFormats.MapItem): self.mapItemDropped.emit(mimeData, dropPosition, face) elif mimeData.hasUrls: self.urlsDropped.emit(mimeData, dropPosition, face) def keyPressEvent(self, event): self.augmentKeyEvent(event) self.pressedKeys.add(event.key()) for action in self.viewActions: if action.matchKeyEvent(event): action.keyPressEvent(event) def keyReleaseEvent(self, event): self.augmentKeyEvent(event) self.pressedKeys.discard(event.key()) for action in self.viewActions: if action.matchKeyEvent(event): action.keyReleaseEvent(event) def mousePressEvent(self, event): self.augmentMouseEvent(event) for action in self.viewActions: if action.button & event.button(): if action.matchModifiers(event): if not action.key or action.key in self.pressedKeys: action.mousePressEvent(event) def mouseMoveEvent(self, event): self.augmentMouseEvent(event) for action in self.viewActions: if not action.button or action.button == event.buttons() or action.button & event.buttons(): # Important! mouseMove checks event.buttons(), press and release check event.button() if action.matchModifiers(event): if not action.key or action.key in self.pressedKeys: action.mouseMoveEvent(event) self.cursorMoved.emit(event) self.update() def mouseReleaseEvent(self, event): self.augmentMouseEvent(event) for action in self.viewActions: if action.button & event.button(): if action.matchModifiers(event): if not action.key or action.key in self.pressedKeys: action.mouseReleaseEvent(event) wheelPos = 0 def wheelEvent(self, event): self.augmentMouseEvent(event) for action in self.viewActions: if action.acceptsMouseWheel and ((action.modifiers & event.modifiers()) or action.modifiers == event.modifiers()): self.wheelPos += event.delta() # event.delta reports eighths of a degree. a standard wheel tick is 15 degrees, or 120 eighths. # keep count of wheel position and emit an event for each 15 degrees turned. # xxx will we ever need sub-click precision for wheel events? clicks = 0 while self.wheelPos >= 120: self.wheelPos -= 120 clicks += 1 while self.wheelPos <= -120: self.wheelPos += 120 clicks -= 1 if action.button == action.WHEEL_UP and clicks > 0: for i in range(abs(clicks)): action.keyPressEvent(event) if action.button == action.WHEEL_DOWN and clicks < 0: for i in range(abs(clicks)): action.keyPressEvent(event) def augmentMouseEvent(self, event): x, y = event.x(), event.y() return self.augmentEvent(x, y, event) def augmentKeyEvent(self, event): globalPos = QtGui.QCursor.pos() mousePos = self.mapFromGlobal(globalPos) x = mousePos.x() y = mousePos.y() # xxx fake position of mouse event -- need to use mcedit internal event object already event.x = lambda: x event.y = lambda: y return self.augmentEvent(x, y, event) @profiler.function def augmentEvent(self, x, y, event): ray = self.rayAtPosition(x, y) event.ray = ray event.view = self position, face = self.rayCastInView(ray) self.mouseBlockPos = event.blockPosition = position self.mouseBlockFace = event.blockFace = face self.mouseRay = ray # --- OpenGL support --- def initializeGL(self, *args, **kwargs): GL.glEnableClientState(GL.GL_VERTEX_ARRAY) GL.glAlphaFunc(GL.GL_NOTEQUAL, 0) GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA) GL.glEnable(GL.GL_DEPTH_TEST) def resizeGL(self, width, height): GL.glViewport(0, 0, width, height) self._updateMatrices() maxFPS = 45 @profiler.function def glDraw(self, *args, **kwargs): if not self.bufferSwapDone: return frameInterval = 1.0 / self.maxFPS if time.time() - self.frameSamples[-1] < frameInterval: return super(WorldView, self).glDraw(*args, **kwargs) shouldRender = True def paintGL(self): if not self.shouldRender: return try: with profiler.context("paintGL: %s" % self): self.frameSamples.append(time.time()) if self.textureAtlas: self.textureAtlas.update() with profiler.context("renderScene"): rendernode.renderScene(self.renderGraph) if THREADED_BUFFER_SWAP: self.doneCurrent() self.bufferSwapDone = False self.doSwapBuffers.emit() except: self.shouldRender = False raise def swapDone(self): self.bufferSwapDone = True @property def fps(self): samples = 3 if len(self.frameSamples) <= samples: return 0.0 return (samples - 1) / (self.frameSamples[-1] - self.frameSamples[-samples]) # --- Screen<->world space conversion --- def rayCastInView(self, ray): try: result = raycast.rayCastInBounds(ray, self.dimension, maxDistance=200) position, face = result except (raycast.MaxDistanceError, ValueError): # GL.glReadBuffer(GL.GL_BACK) # pixel = GL.glReadPixels(x, self.height() - y, 1, 1, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT) # depth = -1 + 2 * pixel[0, 0] # p = self.pointsAtPositions((x, y, depth))[0] # # face = faces.FaceYIncreasing # position = p.intfloor() defaultDistance = 200 position = (ray.point + ray.vector * defaultDistance).intfloor() face = faces.FaceUp return position, face def rayAtPosition(self, x, y): """ Given coordinates in screen space, return a ray in 3D space. Parameters: x and y are coordinates local to this QWidget :rtype: Ray """ p0, p1 = self.pointsAtPositions((x, y, 0.0), (x, y, 1.0)) return Ray(p0, (p1 - p0).normalize()) def rayAtCenter(self): return self.rayAtPosition(self.width()/2, self.height()/2) def pointsAtPositions(self, *screenPoints): w = float(self.width()) h = float(self.height()) matrix = self.matrixState.projection * self.matrixState.modelview inverse, ok = matrix.inverted() if not ok or 0 in (w, h): return [Vector(0, 0, 0) for i in screenPoints] def _unproject(): for x, y, z in screenPoints: x = float(x) y = float(y) y = h - y x = 2 * x / w - 1 y = 2 * y / h - 1 def v(p): return Vector(p.x(), p.y(), p.z()) yield v(inverse.map(QtGui.QVector3D(x, y, z))) return list(_unproject()) def unprojectAtHeight(self, x, y, h): """ Given coordinates in screen space, find the corresponding point in 3D space. Like rayAtPosition, but the third parameter is a height value in 3D space. """ ray = self.rayAtPosition(x, y) return ray.atHeight(h) # --- Chunk loading --- _chunkIter = None def resetLoadOrder(self): self._chunkIter = None def makeChunkIter(self): x, y, z = self.viewCenter() return iterateChunks(x, z, 1 + max(self.width() * self.scale, self.height() * self.scale) // 32) def requestChunk(self): if self._chunkIter is None: self._chunkIter = self.makeChunkIter() try: for c in self._chunkIter: if self.worldScene.wantsChunk(c): return c except StopIteration: pass def wantsChunk(self, c): if not self.worldScene.wantsChunk(c): return False if hasattr(self, 'frustum'): point = [ c[0] * 16 + 8, self.dimension.bounds.miny + self.dimension.bounds.height / 2, c[1] * 16 + 8, 1.0 ] return self.frustum.visible1(point=point, radius=self.dimension.bounds.height / 2) return True def chunkNotPresent(self, cPos): self.worldScene.chunkNotPresent(cPos) def recieveChunk(self, chunk): t = time.time() if self.lastAutoUpdate + self.autoUpdateInterval < t: self.lastAutoUpdate = t log.debug("update(): receivedChunk %s %s", self, chunk) self.update() with profiler.getProfiler().context("preloadCulling"): if hasattr(self, 'frustum'): cx, cz = chunk.chunkPosition points = [(cx * 16 + 8, h + 8, cz * 16 + 8, 1.0) for h in chunk.sectionPositions()] points = numpy.array(points) visibleSections = self.frustum.visible(points, radius=8 * math.sqrt(2)) else: visibleSections = None return self.worldScene.workOnChunk(chunk, visibleSections) def chunkInvalid(self, (cx, cz), deleted): self.worldScene.invalidateChunk(cx, cz) if deleted: self.loadableChunksNode.dirty = True self.resetLoadOrder()
class BrushTool(EditorTool): name = "Brush" iconName = "brush" maxBrushSize = 512 modifiesWorld = True def __init__(self, editorSession, *args, **kwargs): super(BrushTool, self).__init__(editorSession, *args, **kwargs) self.toolWidget = BrushToolWidget() self.brushMode = None self.brushLoader = None self.brushModesByName = {cls.name:cls(self) for cls in BrushModeClasses} brushModes = self.brushModesByName.values() self.toolWidget.brushModeInput.setModes(brushModes) BrushModeSetting.connectAndCall(self.modeSettingChanged) self.cursorWorldScene = None self.cursorBoxNode = None self.cursorNode = Node("brushCursor") self.cursorTranslate = Translate() self.cursorNode.addState(self.cursorTranslate) self.toolWidget.xSpinSlider.setMinimum(1) self.toolWidget.ySpinSlider.setMinimum(1) self.toolWidget.zSpinSlider.setMinimum(1) self.toolWidget.xSpinSlider.valueChanged.connect(self.setX) self.toolWidget.ySpinSlider.valueChanged.connect(self.setY) self.toolWidget.zSpinSlider.valueChanged.connect(self.setZ) self.toolWidget.brushShapeInput.shapeChanged.connect(self.updateCursor) self.toolWidget.brushShapeInput.shapeOptionsChanged.connect(self.updateCursor) self.brushSize = BrushSizeSetting.value(QtGui.QVector3D(5, 5, 5)).toTuple() # calls updateCursor self.toolWidget.xSpinSlider.setValue(self.brushSize[0]) self.toolWidget.ySpinSlider.setValue(self.brushSize[1]) self.toolWidget.zSpinSlider.setValue(self.brushSize[2]) self.toolWidget.hoverSpinSlider.setValue(1) self.dragPoints = [] @property def hoverDistance(self): return self.toolWidget.hoverSpinSlider.value() _brushSize = (0, 0, 0) @property def brushSize(self): return self._brushSize @brushSize.setter def brushSize(self, value): self._brushSize = value BrushSizeSetting.setValue(QtGui.QVector3D(*self.brushSize)) self.updateCursor() def setX(self, val): x, y, z = self.brushSize x = float(val) self.brushSize = x, y, z def setY(self, val): x, y, z = self.brushSize y = float(val) self.brushSize = x, y, z def setZ(self, val): x, y, z = self.brushSize z = float(val) self.brushSize = x, y, z def hoverPosition(self, event): if event.blockPosition: vector = (event.blockFace.vector * self.hoverDistance) pos = event.blockPosition + vector return pos def mousePress(self, event): self.dragPoints[:] = [] pos = self.hoverPosition(event) if pos: self.dragPoints.append(pos) def mouseMove(self, event): pos = self.hoverPosition(event) if pos: self.cursorTranslate.translateOffset = pos def mouseDrag(self, event): p2 = self.hoverPosition(event) if p2: if len(self.dragPoints): p1 = self.dragPoints.pop(-1) points = list(bresenham.bresenham(p1, p2)) self.dragPoints.extend(points) else: self.dragPoints.append(p2) def mouseRelease(self, event): if not len(self.dragPoints): pos = self.hoverPosition(event) if pos: self.dragPoints.append(pos) if len(self.dragPoints): dragPoints = sorted(set(self.dragPoints)) self.dragPoints[:] = [] command = BrushCommand(self.editorSession, dragPoints, self.options) self.editorSession.pushCommand(command) @property def options(self): options = {'brushSize': self.brushSize, 'brushShape': self.brushShape, 'brushMode': self.brushMode, 'brushHollow': self.brushHollow} options.update(self.brushMode.getOptions()) return options def modeSettingChanged(self, value): self.brushMode = self.brushModesByName[value] stack = self.toolWidget.modeOptionsStack while stack.count(): stack.removeWidget(stack.widget(0)) if self.brushMode.optionsWidget: stack.addWidget(self.brushMode.optionsWidget) @property def brushShape(self): return self.toolWidget.brushShapeInput.currentShape @property def brushHollow(self): return self.toolWidget.hollowCheckBox.isChecked() def updateCursor(self): log.info("Updating brush cursor") if self.cursorWorldScene: self.brushLoader.timer.stop() self.cursorNode.removeChild(self.cursorWorldScene) self.cursorWorldScene = None if self.cursorBoxNode: self.cursorNode.removeChild(self.cursorBoxNode) self.cursorBoxNode = None cursorLevel = self.brushMode.createCursorLevel(self) if cursorLevel is not None: self.cursorWorldScene = worldscene.WorldScene(cursorLevel, self.editorSession.textureAtlas) self.cursorWorldScene.depthOffset.depthOffset = DepthOffsets.PreviewRenderer self.cursorNode.addChild(self.cursorWorldScene) self.brushLoader = WorldLoader(self.cursorWorldScene) self.brushLoader.startLoader() cursorBox = self.brushMode.brushBoxForPoint((0, 0, 0), self.options) if cursorBox is not None: self.cursorBoxNode = SelectionBoxNode() self.cursorBoxNode.selectionBox = cursorBox self.cursorBoxNode.filled = False self.cursorNode.addChild(self.cursorBoxNode)
def __init__(self, pendingImport, textureAtlas, hasHandle=True): """ A scenegraph node displaying an object that will be imported later, including live and deferred views of the object with transformed items, and a BoxHandle for moving the item. Parameters ---------- pendingImport: PendingImport The object to be imported. The PendingImportNode responds to changes in this object's position, rotation, and scale. textureAtlas: TextureAtlas The textures and block models used to render the preview of the object. Attributes ---------- basePosition: Vector The pre-transform position of the pending import. This is equal to `self.pendingImport.basePosition` except when the node is currently being dragged. transformedPosition: Vector The post-transform position of the pending import. This is equal to `self.pendingImport.importPos` except when the node is currently being dragged, scaled, or rotated. """ super(PendingImportNode, self).__init__() self.textureAtlas = textureAtlas self.pendingImport = pendingImport self.hasHandle = hasHandle dim = pendingImport.sourceDim self.transformedPosition = Vector(0, 0, 0) # worldScene is contained by rotateNode, and # translates the world scene back to 0, 0, 0 so the rotateNode can # rotate it around the anchor, and the plainSceneNode can translate # it to the current position. self.worldScene = WorldScene(dim, textureAtlas, bounds=pendingImport.selection) self.worldScene.depthOffset.depthOffset = DepthOffsets.PreviewRenderer self.worldSceneTranslate = Translate(-self.pendingImport.selection.origin) self.worldScene.addState(self.worldSceneTranslate) # rotateNode is used to rotate the non-transformed preview during live rotation self.rotateNode = Rotate3DNode() self.rotateNode.setAnchor(self.pendingImport.selection.size * 0.5) self.rotateNode.addChild(self.worldScene) # plainSceneNode contains the non-transformed preview of the imported # object, including its world scene. This preview will be rotated model-wise # while the user is dragging the rotate controls. self.plainSceneNode = Node("plainScene") self.positionTranslate = Translate() self.plainSceneNode.addState(self.positionTranslate) self.plainSceneNode.addChild(self.rotateNode) self.addChild(self.plainSceneNode) # transformedSceneNode contains the transformed preview of the imported # object, including a world scene that displays the object wrapped by a # DimensionTransform. self.transformedSceneNode = Node("transformedScene") self.transformedSceneTranslate = Translate() self.transformedSceneNode.addState(self.transformedSceneTranslate) self.transformedWorldScene = None self.addChild(self.transformedSceneNode) box = BoundingBox(pendingImport.importPos, pendingImport.importBounds.size) if hasHandle: # handleNode displays a bounding box that can be moved around, and responds # to mouse events. self.handleNode = BoxHandle() self.handleNode.bounds = box self.handleNode.resizable = False self.boxNode = None else: # boxNode displays a plain, non-movable bounding box self.boxNode = SelectionBoxNode() self.boxNode.wireColor = (1, 1, 1, .2) self.boxNode.filled = False self.handleNode = None self.addChild(self.boxNode) self.updateTransformedScene() self.basePosition = pendingImport.basePosition if hasHandle: self.handleNode.boundsChanged.connect(self.handleBoundsChanged) self.handleNode.boundsChangedDone.connect(self.handleBoundsChangedDone) self.addChild(self.handleNode) # loads the non-transformed world scene asynchronously. self.loader = WorldLoader(self.worldScene, list(pendingImport.selection.chunkPositions())) self.loader.startLoader(0.1 if self.hasHandle else 0.0) self.pendingImport.positionChanged.connect(self.setPosition) self.pendingImport.rotationChanged.connect(self.setRotation)
class BrushTool(EditorTool): name = "Brush" iconName = "brush" maxBrushSize = 512 def __init__(self, editorSession, *args, **kwargs): super(BrushTool, self).__init__(editorSession, *args, **kwargs) self.toolWidget = BrushToolWidget() self.brushMode = None self.brushLoader = None self.brushModesByName = {cls.name:cls() for cls in BrushModeClasses} modes = self.brushModesByName.values() modes.sort(key=lambda m: m.name) self.toolWidget.brushModeInput.setModes(modes) BrushModeSetting.connectAndCall(self.modeSettingChanged) self.cursorWorldScene = None self.cursorNode = Node("brushCursor") self.cursorTranslate = Translate() self.cursorNode.addState(self.cursorTranslate) self.toolWidget.xSpinSlider.setMinimum(1) self.toolWidget.ySpinSlider.setMinimum(1) self.toolWidget.zSpinSlider.setMinimum(1) self.toolWidget.xSpinSlider.valueChanged.connect(self.setX) self.toolWidget.ySpinSlider.valueChanged.connect(self.setY) self.toolWidget.zSpinSlider.valueChanged.connect(self.setZ) self.toolWidget.brushShapeInput.shapeChanged.connect(self.updateCursor) self.toolWidget.brushShapeInput.shapeOptionsChanged.connect(self.updateCursor) self.fillBlock = editorSession.worldEditor.blocktypes["stone"] self.brushSize = BrushSizeSetting.value(QtGui.QVector3D(5, 5, 5)).toTuple() # calls updateCursor self.toolWidget.xSpinSlider.setValue(self.brushSize[0]) self.toolWidget.ySpinSlider.setValue(self.brushSize[1]) self.toolWidget.zSpinSlider.setValue(self.brushSize[2]) @property def hoverDistance(self): return self.toolWidget.hoverSpinSlider.value() _brushSize = (0, 0, 0) @property def brushSize(self): return self._brushSize @brushSize.setter def brushSize(self, value): self._brushSize = value BrushSizeSetting.setValue(QtGui.QVector3D(*self.brushSize)) self.updateCursor() def setX(self, val): x, y, z = self.brushSize x = float(val) self.brushSize = x, y, z def setY(self, val): x, y, z = self.brushSize y = float(val) self.brushSize = x, y, z def setZ(self, val): x, y, z = self.brushSize z = float(val) self.brushSize = x, y, z def setBlocktypes(self, types): if len(types) == 0: return self.fillBlock = types[0] self.updateCursor() def mousePress(self, event): pos = event.blockPosition vector = (event.blockFace.vector * self.hoverDistance) command = BrushCommand(self.editorSession, [pos + vector], self.options) self.editorSession.pushCommand(command) def mouseMove(self, event): if event.blockPosition: vector = (event.blockFace.vector * self.hoverDistance) assert isinstance(vector, Vector), "vector isa %s" % type(vector) self.cursorTranslate.translateOffset = event.blockPosition + vector @property def options(self): options = {'brushSize': self.brushSize, 'brushShape': self.brushShape, 'brushMode': self.brushMode} options.update(self.brushMode.getOptions()) return options def modeSettingChanged(self, value): self.brushMode = self.brushModesByName[value] stack = self.toolWidget.modeOptionsStack while stack.count(): stack.removeWidget(stack.widget(0)) widget = self.brushMode.createOptionsWidget(self) if widget: stack.addWidget(widget) @property def brushShape(self): return self.toolWidget.brushShapeInput.currentShape def updateCursor(self): log.info("Updating brush cursor") if self.cursorWorldScene: self.brushLoader.timer.stop() self.cursorNode.removeChild(self.cursorWorldScene) self.cursorNode.removeChild(self.cursorBoxNode) cursorLevel = self.brushMode.createCursorLevel(self) cursorBox = self.brushMode.brushBoxForPoint((0, 0, 0), self.options) self.cursorBoxNode = SelectionBoxNode() self.cursorBoxNode.selectionBox = cursorBox self.cursorBoxNode.filled = False self.cursorWorldScene = worldscene.WorldScene(cursorLevel, self.editorSession.textureAtlas) self.cursorWorldScene.depthOffset.depthOffset = DepthOffsets.PreviewRenderer self.cursorNode.addChild(self.cursorWorldScene) self.cursorNode.addChild(self.cursorBoxNode) self.brushLoader = WorldLoader(self.cursorWorldScene) self.brushLoader.startLoader()
class PendingImportNode(Node, QtCore.QObject): __node_id_counter = 0 def __init__(self, pendingImport, textureAtlas, hasHandle=True): """ A scenegraph node displaying an object that will be imported later, including live and deferred views of the object with transformed items, and a BoxHandle for moving the item. Parameters ---------- pendingImport: PendingImport The object to be imported. The PendingImportNode responds to changes in this object's position, rotation, and scale. textureAtlas: TextureAtlas The textures and block models used to render the preview of the object. hasHandle: bool True if this import node should have a user-interactive BoxHandle associated with it. This is False for the extra copies displayed by a repeated clone. Attributes ---------- basePosition: Vector The pre-transform position of the pending import. This is equal to `self.pendingImport.basePosition` except when the node is currently being dragged. transformedPosition: Vector The post-transform position of the pending import. This is equal to `self.pendingImport.importPos` except when the node is currently being dragged, scaled, or rotated. """ super(PendingImportNode, self).__init__() self.textureAtlas = textureAtlas self.pendingImport = pendingImport self.hasHandle = hasHandle dim = pendingImport.sourceDim self.transformedPosition = Vector(0, 0, 0) # worldScene is contained by rotateNode, and # translates the world scene back to 0, 0, 0 so the rotateNode can # rotate it around the anchor, and the plainSceneNode can translate # it to the current position. self.worldScene = WorldScene(dim, textureAtlas, bounds=pendingImport.selection) self.worldScene.depthOffset.depthOffset = DepthOffsets.PreviewRenderer self.worldSceneTranslate = Translate(-self.pendingImport.selection.origin) self.worldScene.addState(self.worldSceneTranslate) # rotateNode is used to rotate the non-transformed preview during live rotation self.rotateNode = Rotate3DNode() self.rotateNode.setAnchor(self.pendingImport.selection.size * 0.5) self.rotateNode.addChild(self.worldScene) self.scaleNode = Scale3DNode() self.scaleNode.setAnchor(self.pendingImport.selection.size * 0.5) self.scaleNode.addChild(self.rotateNode) # plainSceneNode contains the non-transformed preview of the imported # object, including its world scene. This preview will be rotated model-wise # while the user is dragging the rotate controls. self.plainSceneNode = Node("plainScene") self.positionTranslate = Translate() self.plainSceneNode.addState(self.positionTranslate) self.plainSceneNode.addChild(self.scaleNode) self.addChild(self.plainSceneNode) # transformedSceneNode contains the transformed preview of the imported # object, including a world scene that displays the object wrapped by a # DimensionTransform. self.transformedSceneNode = Node("transformedScene") self.transformedSceneTranslate = Translate() self.transformedSceneNode.addState(self.transformedSceneTranslate) self.transformedWorldScene = None self.addChild(self.transformedSceneNode) box = BoundingBox(pendingImport.importPos, pendingImport.importBounds.size) if hasHandle: # handleNode displays a bounding box that can be moved around, and responds # to mouse events. self.handleNode = BoxHandle() self.handleNode.bounds = box self.handleNode.resizable = False self.boxNode = None else: # boxNode displays a plain, non-movable bounding box self.boxNode = SelectionBoxNode() self.boxNode.wireColor = (1, 1, 1, .2) self.boxNode.filled = False self.handleNode = None self.addChild(self.boxNode) self.updateTransformedScene() self.basePosition = pendingImport.basePosition if hasHandle: self.handleNode.boundsChanged.connect(self.handleBoundsChanged) self.handleNode.boundsChangedDone.connect(self.handleBoundsChangedDone) self.addChild(self.handleNode) # loads the non-transformed world scene asynchronously. self.loader = WorldLoader(self.worldScene, list(pendingImport.selection.chunkPositions())) self.loader.startLoader(0.1 if self.hasHandle else 0.0) self.pendingImport.positionChanged.connect(self.setPosition) self.pendingImport.rotationChanged.connect(self.setRotation) self.pendingImport.scaleChanged.connect(self.setScale) # Emitted when the user finishes dragging the box handle and releases the mouse # button. Arguments are (newPosition, oldPosition). importMoved = QtCore.Signal(object, object) # Emitted while the user is dragging the box handle. Argument is the box origin. importIsMoving = QtCore.Signal(object) def handleBoundsChangedDone(self, bounds, oldBounds): point = self.getBaseFromTransformed(bounds.origin) oldPoint = self.getBaseFromTransformed(oldBounds.origin) if point != oldPoint: self.importMoved.emit(point, oldPoint) def handleBoundsChanged(self, bounds): log.info("handleBoundsChanged: %s", bounds) self.setPreviewBasePosition(bounds.origin) def setPreviewBasePosition(self, origin): point = self.getBaseFromTransformed(origin) if self.basePosition != point: self.basePosition = point self.importIsMoving.emit(point) def getBaseFromTransformed(self, point): return point - self.pendingImport.transformOffset def setPreviewRotation(self, rots): self.plainSceneNode.visible = True self.transformedSceneNode.visible = False self.rotateNode.setRotation(rots) def setRotation(self, rots): self.updateTransformedScene() self.updateBoxHandle() self.rotateNode.setRotation(rots) def setPreviewScale(self, scale): self.plainSceneNode.visible = True self.transformedSceneNode.visible = False self.scaleNode.setScale(scale) def setScale(self, scale): self.updateTransformedScene() self.updateBoxHandle() self.scaleNode.setScale(scale) def updateTransformedScene(self): if self.pendingImport.transformedDim is not None: log.info("Showing transformed scene") self.plainSceneNode.visible = False self.transformedSceneNode.visible = True if self.transformedWorldScene: self.transformedSceneNode.removeChild(self.transformedWorldScene) self.transformedWorldScene = WorldScene(self.pendingImport.transformedDim, self.textureAtlas) self.transformedWorldScene.depthOffset.depthOffset = DepthOffsets.PreviewRenderer self.transformedSceneNode.addChild(self.transformedWorldScene) self.updateTransformedPosition() cPos = list(self.pendingImport.transformedDim.chunkPositions()) self.loader = WorldLoader(self.transformedWorldScene, cPos) # ALERT!: self.hasHandle is overloaded with the meaning: # "not the first clone in a repeated clone" self.loader.startLoader(0.1 if self.hasHandle else 0.0) else: log.info("Hiding transformed scene") self.plainSceneNode.visible = True self.transformedSceneNode.visible = False if self.transformedWorldScene: self.transformedSceneNode.removeChild(self.transformedWorldScene) self.transformedWorldScene = None def updateTransformedPosition(self): self.transformedPosition = self.basePosition + self.pendingImport.transformOffset self.transformedSceneTranslate.translateOffset = self.transformedPosition - self.pendingImport.importDim.bounds.origin def updateBoxHandle(self): if self.transformedWorldScene is None: bounds = BoundingBox(self.basePosition, self.pendingImport.bounds.size) else: origin = self.transformedPosition bounds = BoundingBox(origin, self.pendingImport.importBounds.size) #if self.handleNode.bounds.size != bounds.size: if self.hasHandle: self.handleNode.bounds = bounds else: self.boxNode.selectionBox = bounds @property def basePosition(self): return self.positionTranslate.translateOffset @basePosition.setter def basePosition(self, value): value = Vector(*value) if value == self.positionTranslate.translateOffset: return self.positionTranslate.translateOffset = value self.updateTransformedPosition() self.updateBoxHandle() def setPosition(self, pos): self.basePosition = pos # --- Mouse events --- # inherit from BoxHandle? def mouseMove(self, event): if not self.hasHandle: return self.handleNode.mouseMove(event) def mousePress(self, event): if not self.hasHandle: return self.handleNode.mousePress(event) def mouseRelease(self, event): if not self.hasHandle: return self.handleNode.mouseRelease(event)
def __init__(self, pendingImport, textureAtlas, hasHandle=True): """ A scenegraph node displaying an object that will be imported later, including live and deferred views of the object with transformed items, and a BoxHandle for moving the item. Parameters ---------- pendingImport: PendingImport The object to be imported. The PendingImportNode responds to changes in this object's position, rotation, and scale. textureAtlas: TextureAtlas The textures and block models used to render the preview of the object. hasHandle: bool True if this import node should have a user-interactive BoxHandle associated with it. This is False for the extra copies displayed by a repeated clone. Attributes ---------- basePosition: Vector The pre-transform position of the pending import. This is equal to `self.pendingImport.basePosition` except when the node is currently being dragged. transformedPosition: Vector The post-transform position of the pending import. This is equal to `self.pendingImport.importPos` except when the node is currently being dragged, scaled, or rotated. """ super(PendingImportNode, self).__init__() self.textureAtlas = textureAtlas self.pendingImport = pendingImport self.hasHandle = hasHandle dim = pendingImport.sourceDim self.transformedPosition = Vector(0, 0, 0) # worldScene is contained by rotateNode, and # translates the world scene back to 0, 0, 0 so the rotateNode can # rotate it around the anchor, and the plainSceneNode can translate # it to the current position. self.worldScene = WorldScene(dim, textureAtlas, bounds=pendingImport.selection) self.worldScene.depthOffset.depthOffset = DepthOffsets.PreviewRenderer self.worldSceneTranslate = Translate(-self.pendingImport.selection.origin) self.worldScene.addState(self.worldSceneTranslate) # rotateNode is used to rotate the non-transformed preview during live rotation self.rotateNode = Rotate3DNode() self.rotateNode.setAnchor(self.pendingImport.selection.size * 0.5) self.rotateNode.addChild(self.worldScene) self.scaleNode = Scale3DNode() self.scaleNode.setAnchor(self.pendingImport.selection.size * 0.5) self.scaleNode.addChild(self.rotateNode) # plainSceneNode contains the non-transformed preview of the imported # object, including its world scene. This preview will be rotated model-wise # while the user is dragging the rotate controls. self.plainSceneNode = Node("plainScene") self.positionTranslate = Translate() self.plainSceneNode.addState(self.positionTranslate) self.plainSceneNode.addChild(self.scaleNode) self.addChild(self.plainSceneNode) # transformedSceneNode contains the transformed preview of the imported # object, including a world scene that displays the object wrapped by a # DimensionTransform. self.transformedSceneNode = Node("transformedScene") self.transformedSceneTranslate = Translate() self.transformedSceneNode.addState(self.transformedSceneTranslate) self.transformedWorldScene = None self.addChild(self.transformedSceneNode) box = BoundingBox(pendingImport.importPos, pendingImport.importBounds.size) if hasHandle: # handleNode displays a bounding box that can be moved around, and responds # to mouse events. self.handleNode = BoxHandle() self.handleNode.bounds = box self.handleNode.resizable = False self.boxNode = None else: # boxNode displays a plain, non-movable bounding box self.boxNode = SelectionBoxNode() self.boxNode.wireColor = (1, 1, 1, .2) self.boxNode.filled = False self.handleNode = None self.addChild(self.boxNode) self.updateTransformedScene() self.basePosition = pendingImport.basePosition if hasHandle: self.handleNode.boundsChanged.connect(self.handleBoundsChanged) self.handleNode.boundsChangedDone.connect(self.handleBoundsChangedDone) self.addChild(self.handleNode) # loads the non-transformed world scene asynchronously. self.loader = WorldLoader(self.worldScene, list(pendingImport.selection.chunkPositions())) self.loader.startLoader(0.1 if self.hasHandle else 0.0) self.pendingImport.positionChanged.connect(self.setPosition) self.pendingImport.rotationChanged.connect(self.setRotation) self.pendingImport.scaleChanged.connect(self.setScale)
def __init__(self, *args, **kwargs): EditorTool.__init__(self, *args, **kwargs) self.livePreview = False self.blockPreview = False self.glPreview = True toolWidget = QtGui.QWidget() self.toolWidget = toolWidget column = [] self.generatorTypes = [ pluginClass(self) for pluginClass in GeneratePlugins.registeredPlugins ] self.currentGenerator = None if len(self.generatorTypes): self.currentGenerator = self.generatorTypes[0] self.generatorTypeInput = QtGui.QComboBox() self.generatorTypesChanged() self.generatorTypeInput.currentIndexChanged.connect( self.currentTypeChanged) self.livePreviewCheckbox = QtGui.QCheckBox("Live Preview") self.livePreviewCheckbox.setChecked(self.livePreview) self.livePreviewCheckbox.toggled.connect(self.livePreviewToggled) self.blockPreviewCheckbox = QtGui.QCheckBox("Block Preview") self.blockPreviewCheckbox.setChecked(self.blockPreview) self.blockPreviewCheckbox.toggled.connect(self.blockPreviewToggled) self.glPreviewCheckbox = QtGui.QCheckBox("GL Preview") self.glPreviewCheckbox.setChecked(self.glPreview) self.glPreviewCheckbox.toggled.connect(self.glPreviewToggled) self.optionsHolder = QtGui.QStackedWidget() self.optionsHolder.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) self.generateButton = QtGui.QPushButton(self.tr("Generate")) self.generateButton.clicked.connect(self.generateClicked) column.append(self.generatorTypeInput) column.append(self.livePreviewCheckbox) column.append(self.blockPreviewCheckbox) column.append(self.glPreviewCheckbox) column.append(self.optionsHolder) column.append(self.generateButton) self.toolWidget.setLayout(Column(*column)) self.overlayNode = scenenode.Node("generateOverlay") self.sceneHolderNode = Node("sceneHolder") self.sceneTranslate = Translate() self.sceneHolderNode.addState(self.sceneTranslate) self.overlayNode.addChild(self.sceneHolderNode) self.previewNode = None self.boxHandleNode = BoxHandle() self.boxHandleNode.boundsChanged.connect(self.boundsDidChange) self.boxHandleNode.boundsChangedDone.connect(self.boundsDidChangeDone) self.overlayNode.addChild(self.boxHandleNode) self.worldScene = None self.loader = None self.previewBounds = None self.schematicBounds = None self.currentSchematic = None self.currentTypeChanged(0) # Name of last selected generator plugin is saved after unloading # so it can be reselected if it is immediately reloaded self._lastTypeName = None GeneratePlugins.pluginAdded.connect(self.addPlugin) GeneratePlugins.pluginRemoved.connect(self.removePlugin)
class GenerateTool(EditorTool): name = "Generate" iconName = "generate" instantDisplayChunks = 32 def __init__(self, *args, **kwargs): EditorTool.__init__(self, *args, **kwargs) self.livePreview = False self.blockPreview = False self.glPreview = True toolWidget = QtGui.QWidget() self.toolWidget = toolWidget column = [] self.generatorTypes = [pluginClass(self) for pluginClass in _pluginClasses] self.currentGenerator = None if len(self.generatorTypes): self.currentGenerator = self.generatorTypes[0] self.generatorTypeInput = QtGui.QComboBox() self.generatorTypesChanged() self.generatorTypeInput.currentIndexChanged.connect(self.currentTypeChanged) self.livePreviewCheckbox = QtGui.QCheckBox("Live Preview") self.livePreviewCheckbox.setChecked(self.livePreview) self.livePreviewCheckbox.toggled.connect(self.livePreviewToggled) self.blockPreviewCheckbox = QtGui.QCheckBox("Block Preview") self.blockPreviewCheckbox.setChecked(self.blockPreview) self.blockPreviewCheckbox.toggled.connect(self.blockPreviewToggled) self.glPreviewCheckbox = QtGui.QCheckBox("GL Preview") self.glPreviewCheckbox.setChecked(self.glPreview) self.glPreviewCheckbox.toggled.connect(self.glPreviewToggled) self.optionsHolder = QtGui.QStackedWidget() self.optionsHolder.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) self.generateButton = QtGui.QPushButton(self.tr("Generate")) self.generateButton.clicked.connect(self.generateClicked) column.append(self.generatorTypeInput) column.append(self.livePreviewCheckbox) column.append(self.blockPreviewCheckbox) column.append(self.glPreviewCheckbox) column.append(self.optionsHolder) column.append(self.generateButton) self.toolWidget.setLayout(Column(*column)) self.overlayNode = scenenode.Node() self.sceneHolderNode = Node() self.sceneTranslate = Translate() self.sceneHolderNode.addState(self.sceneTranslate) self.overlayNode.addChild(self.sceneHolderNode) self.previewNode = None self.boxHandleNode = BoxHandle() self.boxHandleNode.boundsChanged.connect(self.boundsDidChange) self.boxHandleNode.boundsChangedDone.connect(self.boundsDidChangeDone) self.overlayNode.addChild(self.boxHandleNode) self.worldScene = None self.loader = None self.previewBounds = None self.schematicBounds = None self.currentSchematic = None self.currentTypeChanged(0) # Name of last selected generator plugin is saved after unloading # so it can be reselected if it is immediately reloaded self._lastTypeName = None _GeneratePlugins.instance.pluginAdded.connect(self.addPlugin) _GeneratePlugins.instance.pluginRemoved.connect(self.removePlugin) def removePlugin(self, cls): log.info("Removing plugin %s", cls.__name__) self.generatorTypes[:] = [gt for gt in self.generatorTypes if not isinstance(gt, cls)] self.generatorTypesChanged() if self.currentGenerator not in self.generatorTypes: lastTypeName = self.currentGenerator.__class__.__name__ self.currentTypeChanged(0) # resets self._lastTypeName self._lastTypeName = lastTypeName def addPlugin(self, cls): log.info("Adding plugin %s", cls.__name__) self.generatorTypes.append(cls(self)) self.generatorTypesChanged() if self._lastTypeName is not None: if cls.__name__ == self._lastTypeName: self.currentTypeChanged(len(self.generatorTypes)-1) def generatorTypesChanged(self): self.generatorTypeInput.clear() for gt in self.generatorTypes: if hasattr(gt, 'displayName'): displayName = gt.displayName else: displayName = gt.__class__.__name__ self.generatorTypeInput.addItem(displayName, gt) def livePreviewToggled(self, value): self.livePreview = value def blockPreviewToggled(self, value): self.blockPreview = value if value: if self.currentSchematic: self.displaySchematic(self.currentSchematic, self.schematicBounds.origin) else: self.updateBlockPreview() else: self.clearSchematic() def glPreviewToggled(self, value): self.glPreview = value if value: self.updateNodePreview() # xxx cache previewNode? else: self.clearNode() def currentTypeChanged(self, index): # user selected generator type after old type was unloaded, so forget the old type self._lastTypeName = None self.optionsHolder.removeWidget(self.optionsHolder.widget(0)) if index < len(self.generatorTypes): self.currentGenerator = self.generatorTypes[index] self.optionsHolder.addWidget(self.currentGenerator.getOptionsWidget()) self.updatePreview() else: self.currentGenerator = None self.clearSchematic() self.clearNode() log.info("Chose generator %s", repr(self.currentGenerator)) def mousePress(self, event): self.boxHandleNode.mousePress(event) def mouseMove(self, event): self.boxHandleNode.mouseMove(event) def mouseRelease(self, event): self.boxHandleNode.mouseRelease(event) def boundsDidChange(self, bounds): # box still being resized if not self.currentGenerator: return if not self.livePreview: return self.previewBounds = bounds self.currentGenerator.boundsChanged(bounds) self.updatePreview() def boundsDidChangeDone(self, bounds, oldBounds): # box finished resize if not self.currentGenerator: return self.previewBounds = bounds self.schematicBounds = bounds self.currentGenerator.boundsChanged(bounds) self.updatePreview() def clearNode(self): if self.previewNode: self.overlayNode.removeChild(self.previewNode) self.previewNode = None def updatePreview(self): if self.blockPreview: self.updateBlockPreview() else: self.clearSchematic() if self.glPreview: self.updateNodePreview() else: self.clearNode() def updateBlockPreview(self): bounds = self.previewBounds if bounds is not None and bounds.volume > 0: self.generateNextSchematic(bounds) else: self.clearSchematic() def updateNodePreview(self): bounds = self.previewBounds if self.currentGenerator is None: return if bounds is not None and bounds.volume > 0: try: node = self.currentGenerator.getPreviewNode(bounds) except Exception: log.exception("Error while getting scene nodes from generator:") else: if node is not None: self.clearNode() if isinstance(node, list): nodes = node node = scenenode.Node() for c in nodes: node.addChild(c) self.overlayNode.addChild(node) self.previewNode = node def generateNextSchematic(self, bounds): if bounds is None: self.clearSchematic() return if self.currentGenerator is None: return try: schematic = self.currentGenerator.generate(bounds, self.editorSession.worldEditor.blocktypes) self.currentSchematic = schematic self.displaySchematic(schematic, bounds.origin) except Exception as e: log.exception("Error while running generator %s: %s", self.currentGenerator, e) QtGui.QMessageBox.warning(QtGui.qApp.mainWindow, "Error while running generator", "An error occurred while running the generator: \n %s.\n\n" "Traceback: %s" % (e, traceback.format_exc())) self.livePreview = False def displaySchematic(self, schematic, offset): if schematic is not None: dim = schematic.getDimension() if self.worldScene: self.sceneHolderNode.removeChild(self.worldScene) self.loader.timer.stop() self.loader = None atlas = self.editorSession.textureAtlas self.worldScene = WorldScene(dim, atlas) self.sceneTranslate.translateOffset = offset self.sceneHolderNode.addChild(self.worldScene) self.loader = WorldLoader(self.worldScene) if dim.chunkCount() <= self.instantDisplayChunks: exhaust(self.loader.work()) else: self.loader.startLoader() else: self.clearSchematic() def clearSchematic(self): if self.worldScene: self.sceneHolderNode.removeChild(self.worldScene) self.worldScene = None self.loader.timer.stop() self.loader = None def generateClicked(self): if self.currentGenerator is None: return if self.schematicBounds is None: log.info("schematicBounds is None, not generating") return if self.currentSchematic is None: log.info("Generating new schematic for import") currentSchematic = self.currentGenerator.generate(self.schematicBounds, self.editorSession.worldEditor.blocktypes) else: log.info("Importing previously generated schematic.") currentSchematic = self.currentSchematic command = GenerateCommand(self, self.schematicBounds) try: with command.begin(): if currentSchematic is not None: task = self.editorSession.currentDimension.importSchematicIter(currentSchematic, self.schematicBounds.origin) showProgress(self.tr("Importing generated object..."), task) else: task = self.currentGenerator.generateInWorld(self.schematicBounds, self.editorSession.currentDimension) showProgress(self.tr("Generating object in world..."), task) except Exception as e: log.exception("Error while importing or generating in world: %r" % e) command.undo() else: self.editorSession.pushCommand(command)