Clone tool now has rotation settings

This commit is contained in:
David Vierra 2015-10-13 18:59:35 -10:00
parent f3d65b85c8
commit 9a05736a32
2 changed files with 147 additions and 69 deletions

View File

@ -3,6 +3,9 @@
""" """
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
import logging import logging
import numpy
from mcedit2.command import SimpleRevisionCommand from mcedit2.command import SimpleRevisionCommand
from mcedit2.editorsession import PendingImport from mcedit2.editorsession import PendingImport
from mcedit2.editortools import EditorTool from mcedit2.editortools import EditorTool
@ -14,6 +17,7 @@ from mcedit2.util.showprogress import showProgress
from mcedit2.widgets.coord_widget import CoordinateWidget from mcedit2.widgets.coord_widget import CoordinateWidget
from mcedit2.widgets.layout import Column, Row from mcedit2.widgets.layout import Column, Row
from mcedit2.widgets.rotation_widget import RotationWidget from mcedit2.widgets.rotation_widget import RotationWidget
from mceditlib import transform
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -28,11 +32,11 @@ class CloneSelectionCommand(SimpleRevisionCommand):
def undo(self): def undo(self):
super(CloneSelectionCommand, self).undo() super(CloneSelectionCommand, self).undo()
self.cloneTool.pendingClone = None self.cloneTool.mainPendingClone = None
self.cloneTool.editorSession.chooseTool("Select") self.cloneTool.editorSession.chooseTool("Select")
def redo(self): def redo(self):
self.cloneTool.pendingClone = self.pendingImport self.cloneTool.mainPendingClone = self.pendingImport
self.cloneTool.editorSession.chooseTool("Clone") self.cloneTool.editorSession.chooseTool("Clone")
super(CloneSelectionCommand, self).redo() super(CloneSelectionCommand, self).redo()
@ -52,15 +56,33 @@ class CloneOffsetCommand(QtGui.QUndoCommand):
self.cloneTool.clonePosition = self.newPoint self.cloneTool.clonePosition = self.newPoint
class CloneRotateCommand(QtGui.QUndoCommand):
def __init__(self, oldRotation, newRotation, cloneTool):
super(CloneRotateCommand, self).__init__()
self.cloneTool = cloneTool
self.setText(QtGui.qApp.tr("Rotate Cloned Objects"))
self.newRotation = newRotation
self.oldRotation = oldRotation
def undo(self):
self.cloneTool.setRotation(self.oldRotation)
def redo(self):
self.cloneTool.setRotation(self.newRotation)
class CloneFinishCommand(SimpleRevisionCommand): class CloneFinishCommand(SimpleRevisionCommand):
def __init__(self, cloneTool, pendingImport, *args, **kwargs): def __init__(self, cloneTool, pendingImport, originPoint, *args, **kwargs):
super(CloneFinishCommand, self).__init__(cloneTool.editorSession, cloneTool.tr("Finish Clone"), *args, **kwargs) super(CloneFinishCommand, self).__init__(cloneTool.editorSession, cloneTool.tr("Finish Clone"), *args, **kwargs)
self.pendingImport = pendingImport self.pendingImport = pendingImport
self.cloneTool = cloneTool self.cloneTool = cloneTool
self.originPoint = originPoint
self.previousSelection = None
def undo(self): def undo(self):
super(CloneFinishCommand, self).undo() super(CloneFinishCommand, self).undo()
self.cloneTool.pendingClone = self.pendingImport self.cloneTool.mainPendingClone = self.pendingImport
self.cloneTool.originPoint = self.originPoint
self.editorSession.currentSelection = self.previousSelection self.editorSession.currentSelection = self.previousSelection
self.editorSession.chooseTool("Clone") self.editorSession.chooseTool("Clone")
@ -68,11 +90,26 @@ class CloneFinishCommand(SimpleRevisionCommand):
super(CloneFinishCommand, self).redo() super(CloneFinishCommand, self).redo()
self.previousSelection = self.editorSession.currentSelection self.previousSelection = self.editorSession.currentSelection
self.editorSession.currentSelection = self.pendingImport.bounds self.editorSession.currentSelection = self.pendingImport.bounds
self.cloneTool.pendingClone = None self.cloneTool.mainPendingClone = None
self.cloneTool.originPoint = None
self.editorSession.chooseTool("Select") self.editorSession.chooseTool("Select")
class CloneTool(EditorTool): class CloneTool(EditorTool):
"""
Make multiple copies of the selected area. When selected, displays a preview of the
copies and allows the position, repeat count, and transforms to be changed.
Attributes
----------
mainPendingClone : PendingImport
The object currently being cloned.
pendingClones : list of PendingImport
Repeated imports of the object being cloned
"""
iconName = "clone" iconName = "clone"
name = "Clone" name = "Clone"
@ -80,10 +117,12 @@ class CloneTool(EditorTool):
super(CloneTool, self).__init__(editorSession, *args, **kwargs) super(CloneTool, self).__init__(editorSession, *args, **kwargs)
self.originPoint = None self.originPoint = None
self.offsetPoint = None self.rotations = (0, 0, 0)
self.pendingClones = []
self.pendingCloneNodes = [] self.pendingCloneNodes = []
self.mainCloneNode = None self.mainCloneNode = None
self.overlayNode = scenenode.Node() self.overlayNode = scenenode.Node()
self.overlayNode.name = "Clone Overlay" self.overlayNode.name = "Clone Overlay"
@ -102,7 +141,10 @@ class CloneTool(EditorTool):
self.repeatCountInput.valueChanged.connect(self.setRepeatCount) self.repeatCountInput.valueChanged.connect(self.setRepeatCount)
self.rotateRepeatsCheckbox = QtGui.QCheckBox(self.tr("Rotate Repeats")) self.rotateRepeatsCheckbox = QtGui.QCheckBox(self.tr("Rotate Repeats"))
self.rotateRepeatsCheckbox.toggled.connect(self.updateTiling)
self.rotateOffsetCheckbox = QtGui.QCheckBox(self.tr("Rotate Offset")) self.rotateOffsetCheckbox = QtGui.QCheckBox(self.tr("Rotate Offset"))
self.rotateOffsetCheckbox.toggled.connect(self.updateTiling)
self.toolWidget.setLayout(Column(self.pointInput, self.toolWidget.setLayout(Column(self.pointInput,
self.rotationInput, self.rotationInput,
@ -112,83 +154,110 @@ class CloneTool(EditorTool):
confirmButton, confirmButton,
None)) None))
self.pendingClone = None # Do this after creating pointInput to disable inputs self.mainPendingClone = None # Do this after creating pointInput to disable inputs
def pointInputChanged(self, value): def pointInputChanged(self, value):
if self.offsetPoint != value: if self.mainPendingClone.basePosition != value:
self.offsetPoint = value self.mainPendingClone.basePosition = value
self.pendingClone.pos = value
self.updateTiling() self.updateTiling()
def rotationChanged(self, rots, live): def rotationChanged(self, rots, live):
if live: if live:
if self.mainCloneNode: for node in self.pendingCloneNodes:
self.mainCloneNode.setPreviewRotation(rots) node.setPreviewRotation(rots)
self.editorSession.updateView()
else: else:
if self.pendingClone and self.pendingClone.rotation != rots: if self.mainPendingClone and self.mainPendingClone.rotation != rots:
self.pendingClone.rotation = rots command = CloneRotateCommand(self.rotations, rots, self)
self.editorSession.pushCommand(command)
self.updateTiling() self.updateTiling()
def setTileX(self, value):
self.tileX = value
self.updateTiling()
def setTileY(self, value):
self.tileY = value
self.updateTiling()
def setTileZ(self, value):
self.tileZ = value
self.updateTiling()
def setRepeatCount(self, value): def setRepeatCount(self, value):
self.repeatCount = value self.repeatCount = value
self.updateTiling() self.updateTiling()
def setRotation(self, rots):
if self.mainPendingClone is None:
return
else:
self.rotations = rots
self.updateTiling()
def updateTiling(self): def updateTiling(self):
if self.pendingClone is None: if self.mainPendingClone is None:
repeatCount = 0 repeatCount = 0
else: else:
repeatCount = self.repeatCount repeatCount = self.repeatCount
while len(self.pendingCloneNodes) > repeatCount: while len(self.pendingClones) > repeatCount:
node = self.pendingCloneNodes.pop() node = self.pendingCloneNodes.pop()
self.overlayNode.removeChild(node) self.overlayNode.removeChild(node)
while len(self.pendingCloneNodes) < repeatCount: self.pendingClones.pop()
node = PendingImportNode(self.pendingClone, self.editorSession.textureAtlas)
while len(self.pendingClones) < repeatCount:
clone = PendingImport(self.mainPendingClone.sourceDim,
self.mainPendingClone.basePosition,
self.mainPendingClone.selection,
self.mainPendingClone.text + " %d" % len(self.pendingClones))
node = PendingImportNode(clone,
self.editorSession.textureAtlas)
self.pendingClones.append(clone)
self.pendingCloneNodes.append(node) self.pendingCloneNodes.append(node)
self.overlayNode.addChild(node) self.overlayNode.addChild(node)
# This is stupid. # This is stupid.
if self.mainCloneNode: if self.mainCloneNode:
self.mainCloneNode.importMoved.disconnect(self.cloneDidMove) self.mainCloneNode.importMoved.disconnect(self.cloneDidMove)
self.mainCloneNode.importIsMoving.disconnect(self.cloneIsMoving)
if repeatCount > 0: if repeatCount > 0:
self.mainCloneNode = self.pendingCloneNodes[0] self.mainCloneNode = self.pendingCloneNodes[0]
self.mainCloneNode.importMoved.connect(self.cloneDidMove) self.mainCloneNode.importMoved.connect(self.cloneDidMove)
self.mainCloneNode.importIsMoving.connect(self.cloneIsMoving)
else: else:
self.mainCloneNode = None self.mainCloneNode = None
if None not in (self.offsetPoint, self.originPoint): self.updateTilingPositions()
for node, pos in zip(self.pendingCloneNodes, self.getTilingPositions()):
node.pos = pos def updateTilingPositions(self, offsetPoint=None):
if self.originPoint is not None:
for clone, (pos, rots) in zip(self.pendingClones, self.getTilingPositions(offsetPoint)):
clone.basePosition = pos
clone.rotation = rots
self.editorSession.updateView() self.editorSession.updateView()
def getTilingPositions(self): def getTilingPositions(self, offsetPoint=None):
if None not in (self.offsetPoint, self.originPoint): rotateRepeats = self.rotateRepeatsCheckbox.isChecked()
rotateOffsets = self.rotateOffsetCheckbox.isChecked()
rotations = self.rotations
matrix = transform.rotationMatrix((0, 0, 0), *rotations)
matrix = numpy.linalg.inv(matrix)
if offsetPoint is None:
offsetPoint = self.mainPendingClone.basePosition
if None not in (offsetPoint, self.originPoint):
pos = self.originPoint pos = self.originPoint
offset = self.offsetPoint - self.originPoint offset = offsetPoint - self.originPoint
for i in range(self.repeatCount): for i in range(self.repeatCount):
pos = pos + offset pos = pos + offset
yield pos yield pos, rotations
if rotateRepeats:
rotations = [a+b for a,b in zip(rotations, self.rotations)]
# if rotateOffsets:
# # Convert to 4-element column and back
# offset = tuple(offset) + (0, )
# offset = offset * matrix
# offset = tuple(offset.T)[:3]
@property @property
def pendingClone(self): def mainPendingClone(self):
return self._pendingClone return self._pendingClone
@pendingClone.setter @mainPendingClone.setter
def pendingClone(self, pendingImport): def mainPendingClone(self, pendingImport):
log.info("Begin clone: %s", pendingImport) log.info("Begin clone: %s", pendingImport)
self._pendingClone = pendingImport self._pendingClone = pendingImport
self.pointInput.setEnabled(pendingImport is not None) self.pointInput.setEnabled(pendingImport is not None)
@ -196,24 +265,23 @@ class CloneTool(EditorTool):
def toolActive(self): def toolActive(self):
self.editorSession.selectionTool.hideSelectionWalls = True self.editorSession.selectionTool.hideSelectionWalls = True
if self.pendingClone is not None: if self.mainPendingClone is None:
self.pendingClone = None if self.editorSession.currentSelection is None:
return
if self.editorSession.currentSelection is None: # This makes a reference to the latest revision in the editor.
return # If the cloned area is changed between "Clone" and "Confirm", the changed
# blocks will be cloned.
pos = self.editorSession.currentSelection.origin
self.originPoint = pos
pendingImport = PendingImport(self.editorSession.currentDimension, pos,
self.editorSession.currentSelection,
self.tr("<Cloned Object>"))
moveCommand = CloneSelectionCommand(self, pendingImport)
# This makes a reference to the latest revision in the editor. self.editorSession.pushCommand(moveCommand)
# If the cloned area is changed between "Clone" and "Confirm", the changed
# blocks will be moved.
pos = self.editorSession.currentSelection.origin
self.originPoint = pos
self.offsetPoint = pos
pendingImport = PendingImport(self.editorSession.currentDimension, pos,
self.editorSession.currentSelection,
self.tr("<Cloned Object>"))
moveCommand = CloneSelectionCommand(self, pendingImport)
self.editorSession.pushCommand(moveCommand) self.updateTiling()
def toolInactive(self): def toolInactive(self):
self.editorSession.selectionTool.hideSelectionWalls = False self.editorSession.selectionTool.hideSelectionWalls = False
@ -223,31 +291,29 @@ class CloneTool(EditorTool):
self.confirmClone() self.confirmClone()
def confirmClone(self): def confirmClone(self):
if self.pendingClone is None: if self.mainPendingClone is None:
return return
command = CloneFinishCommand(self, self.pendingClone) command = CloneFinishCommand(self, self.mainPendingClone, self.originPoint)
with command.begin(): with command.begin():
# TODO don't use intermediate schematic...
export = self.pendingClone.sourceDim.exportSchematicIter(self.pendingClone.selection)
schematic = showProgress("Copying...", export)
dim = schematic.getDimension()
tasks = [] tasks = []
for pos in self.getTilingPositions(): for clone in self.pendingClones:
task = self.editorSession.currentDimension.copyBlocksIter(dim, dim.bounds, pos, # TODO don't use intermediate schematic...
biomes=True, create=True) destDim = self.editorSession.currentDimension
dim = clone.getSourceForDim(destDim)
task = destDim.copyBlocksIter(dim, dim.bounds, clone.importPos,
biomes=True, create=True, copyAir=False)
tasks.append(task) tasks.append(task)
showProgress(self.tr("Pasting..."), *tasks) showProgress(self.tr("Pasting..."), *tasks)
self.editorSession.pushCommand(command) self.editorSession.pushCommand(command)
self.originPoint = None
@property @property
def clonePosition(self): def clonePosition(self):
return None if self.pendingClone is None else self.pendingClone.pos return None if self.mainPendingClone is None else self.mainPendingClone.basePosition
@clonePosition.setter @clonePosition.setter
def clonePosition(self, value): def clonePosition(self, value):
@ -258,6 +324,7 @@ class CloneTool(EditorTool):
self.pointInput.point = value self.pointInput.point = value
self.pointInputChanged(value) self.pointInputChanged(value)
# --- Mouse events --- # --- Mouse events ---
def mouseMove(self, event): def mouseMove(self, event):
@ -283,3 +350,6 @@ class CloneTool(EditorTool):
if newPoint != oldPoint: if newPoint != oldPoint:
command = CloneOffsetCommand(self, oldPoint, newPoint) command = CloneOffsetCommand(self, oldPoint, newPoint)
self.editorSession.pushCommand(command) self.editorSession.pushCommand(command)
def cloneIsMoving(self, newPoint):
self.updateTilingPositions(newPoint)

View File

@ -10,7 +10,9 @@ from mcedit2.rendering.depths import DepthOffset
from mcedit2.rendering.scenegraph.matrix import TranslateNode, RotateNode from mcedit2.rendering.scenegraph.matrix import TranslateNode, RotateNode
from mcedit2.rendering.scenegraph.scenenode import Node from mcedit2.rendering.scenegraph.scenenode import Node
from mcedit2.rendering.worldscene import WorldScene from mcedit2.rendering.worldscene import WorldScene
from mcedit2.util.showprogress import showProgress
from mcedit2.util.worldloader import WorldLoader from mcedit2.util.worldloader import WorldLoader
from mceditlib.export import extractSchematicFromIter
from mceditlib.geometry import Vector from mceditlib.geometry import Vector
from mceditlib.selection import BoundingBox from mceditlib.selection import BoundingBox
from mceditlib.transform import SelectionTransform, DimensionTransform from mceditlib.transform import SelectionTransform, DimensionTransform
@ -156,6 +158,9 @@ class PendingImportNode(Node, QtCore.QObject):
# button. Arguments are (newPosition, oldPosition). # button. Arguments are (newPosition, oldPosition).
importMoved = QtCore.Signal(object, object) 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): def handleBoundsChangedDone(self, bounds, oldBounds):
point = self.getBaseFromTransformed(bounds.origin) point = self.getBaseFromTransformed(bounds.origin)
oldPoint = self.getBaseFromTransformed(oldBounds.origin) oldPoint = self.getBaseFromTransformed(oldBounds.origin)
@ -166,6 +171,7 @@ class PendingImportNode(Node, QtCore.QObject):
point = self.getBaseFromTransformed(bounds.origin) point = self.getBaseFromTransformed(bounds.origin)
if self.basePosition != point: if self.basePosition != point:
self.basePosition = point self.basePosition = point
self.importIsMoving.emit(point)
def getBaseFromTransformed(self, point): def getBaseFromTransformed(self, point):
offset = self.pendingImport.basePosition - self.pendingImport.importPos offset = self.pendingImport.basePosition - self.pendingImport.importPos
@ -226,10 +232,11 @@ class PendingImportNode(Node, QtCore.QObject):
@basePosition.setter @basePosition.setter
def basePosition(self, value): def basePosition(self, value):
value = Vector(*value)
if value == self.positionTranslateNode.translateOffset: if value == self.positionTranslateNode.translateOffset:
return return
self.positionTranslateNode.translateOffset = Vector(*value) self.positionTranslateNode.translateOffset = value
self.updateTransformedSceneOffset() self.updateTransformedSceneOffset()
self.updateBoxHandle() self.updateBoxHandle()
@ -395,9 +402,10 @@ class PendingImport(QtCore.QObject):
@rotation.setter @rotation.setter
def rotation(self, value): def rotation(self, value):
value = tuple(value)
if self._rotation == value: if self._rotation == value:
return return
self._rotation = Vector(*value) self._rotation = value
self.updateTransform() self.updateTransform()
self.rotationChanged.emit(self._rotation) self.rotationChanged.emit(self._rotation)