Clone tool now has rotation settings
This commit is contained in:
parent
f3d65b85c8
commit
9a05736a32
@ -3,6 +3,9 @@
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
import logging
|
||||
|
||||
import numpy
|
||||
|
||||
from mcedit2.command import SimpleRevisionCommand
|
||||
from mcedit2.editorsession import PendingImport
|
||||
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.layout import Column, Row
|
||||
from mcedit2.widgets.rotation_widget import RotationWidget
|
||||
from mceditlib import transform
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -28,11 +32,11 @@ class CloneSelectionCommand(SimpleRevisionCommand):
|
||||
|
||||
def undo(self):
|
||||
super(CloneSelectionCommand, self).undo()
|
||||
self.cloneTool.pendingClone = None
|
||||
self.cloneTool.mainPendingClone = None
|
||||
self.cloneTool.editorSession.chooseTool("Select")
|
||||
|
||||
def redo(self):
|
||||
self.cloneTool.pendingClone = self.pendingImport
|
||||
self.cloneTool.mainPendingClone = self.pendingImport
|
||||
self.cloneTool.editorSession.chooseTool("Clone")
|
||||
super(CloneSelectionCommand, self).redo()
|
||||
|
||||
@ -52,15 +56,33 @@ class CloneOffsetCommand(QtGui.QUndoCommand):
|
||||
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):
|
||||
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)
|
||||
self.pendingImport = pendingImport
|
||||
self.cloneTool = cloneTool
|
||||
self.originPoint = originPoint
|
||||
self.previousSelection = None
|
||||
|
||||
def undo(self):
|
||||
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.chooseTool("Clone")
|
||||
|
||||
@ -68,11 +90,26 @@ class CloneFinishCommand(SimpleRevisionCommand):
|
||||
super(CloneFinishCommand, self).redo()
|
||||
self.previousSelection = self.editorSession.currentSelection
|
||||
self.editorSession.currentSelection = self.pendingImport.bounds
|
||||
self.cloneTool.pendingClone = None
|
||||
self.cloneTool.mainPendingClone = None
|
||||
self.cloneTool.originPoint = None
|
||||
self.editorSession.chooseTool("Select")
|
||||
|
||||
|
||||
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"
|
||||
name = "Clone"
|
||||
|
||||
@ -80,10 +117,12 @@ class CloneTool(EditorTool):
|
||||
super(CloneTool, self).__init__(editorSession, *args, **kwargs)
|
||||
|
||||
self.originPoint = None
|
||||
self.offsetPoint = None
|
||||
self.rotations = (0, 0, 0)
|
||||
|
||||
self.pendingClones = []
|
||||
self.pendingCloneNodes = []
|
||||
self.mainCloneNode = None
|
||||
|
||||
self.overlayNode = scenenode.Node()
|
||||
self.overlayNode.name = "Clone Overlay"
|
||||
|
||||
@ -102,7 +141,10 @@ class CloneTool(EditorTool):
|
||||
self.repeatCountInput.valueChanged.connect(self.setRepeatCount)
|
||||
|
||||
self.rotateRepeatsCheckbox = QtGui.QCheckBox(self.tr("Rotate Repeats"))
|
||||
self.rotateRepeatsCheckbox.toggled.connect(self.updateTiling)
|
||||
|
||||
self.rotateOffsetCheckbox = QtGui.QCheckBox(self.tr("Rotate Offset"))
|
||||
self.rotateOffsetCheckbox.toggled.connect(self.updateTiling)
|
||||
|
||||
self.toolWidget.setLayout(Column(self.pointInput,
|
||||
self.rotationInput,
|
||||
@ -112,83 +154,110 @@ class CloneTool(EditorTool):
|
||||
confirmButton,
|
||||
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):
|
||||
if self.offsetPoint != value:
|
||||
self.offsetPoint = value
|
||||
self.pendingClone.pos = value
|
||||
if self.mainPendingClone.basePosition != value:
|
||||
self.mainPendingClone.basePosition = value
|
||||
self.updateTiling()
|
||||
|
||||
def rotationChanged(self, rots, live):
|
||||
if live:
|
||||
if self.mainCloneNode:
|
||||
self.mainCloneNode.setPreviewRotation(rots)
|
||||
for node in self.pendingCloneNodes:
|
||||
node.setPreviewRotation(rots)
|
||||
self.editorSession.updateView()
|
||||
else:
|
||||
if self.pendingClone and self.pendingClone.rotation != rots:
|
||||
self.pendingClone.rotation = rots
|
||||
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
|
||||
if self.mainPendingClone and self.mainPendingClone.rotation != rots:
|
||||
command = CloneRotateCommand(self.rotations, rots, self)
|
||||
self.editorSession.pushCommand(command)
|
||||
self.updateTiling()
|
||||
|
||||
def setRepeatCount(self, value):
|
||||
self.repeatCount = value
|
||||
self.updateTiling()
|
||||
|
||||
def setRotation(self, rots):
|
||||
if self.mainPendingClone is None:
|
||||
return
|
||||
else:
|
||||
self.rotations = rots
|
||||
self.updateTiling()
|
||||
|
||||
def updateTiling(self):
|
||||
if self.pendingClone is None:
|
||||
if self.mainPendingClone is None:
|
||||
repeatCount = 0
|
||||
else:
|
||||
repeatCount = self.repeatCount
|
||||
|
||||
while len(self.pendingCloneNodes) > repeatCount:
|
||||
while len(self.pendingClones) > repeatCount:
|
||||
node = self.pendingCloneNodes.pop()
|
||||
self.overlayNode.removeChild(node)
|
||||
while len(self.pendingCloneNodes) < repeatCount:
|
||||
node = PendingImportNode(self.pendingClone, self.editorSession.textureAtlas)
|
||||
self.pendingClones.pop()
|
||||
|
||||
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.overlayNode.addChild(node)
|
||||
|
||||
# This is stupid.
|
||||
if self.mainCloneNode:
|
||||
self.mainCloneNode.importMoved.disconnect(self.cloneDidMove)
|
||||
self.mainCloneNode.importIsMoving.disconnect(self.cloneIsMoving)
|
||||
|
||||
if repeatCount > 0:
|
||||
self.mainCloneNode = self.pendingCloneNodes[0]
|
||||
self.mainCloneNode.importMoved.connect(self.cloneDidMove)
|
||||
self.mainCloneNode.importIsMoving.connect(self.cloneIsMoving)
|
||||
else:
|
||||
self.mainCloneNode = None
|
||||
|
||||
if None not in (self.offsetPoint, self.originPoint):
|
||||
for node, pos in zip(self.pendingCloneNodes, self.getTilingPositions()):
|
||||
node.pos = pos
|
||||
self.updateTilingPositions()
|
||||
|
||||
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()
|
||||
|
||||
def getTilingPositions(self):
|
||||
if None not in (self.offsetPoint, self.originPoint):
|
||||
def getTilingPositions(self, offsetPoint=None):
|
||||
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
|
||||
offset = self.offsetPoint - self.originPoint
|
||||
offset = offsetPoint - self.originPoint
|
||||
for i in range(self.repeatCount):
|
||||
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
|
||||
def pendingClone(self):
|
||||
def mainPendingClone(self):
|
||||
return self._pendingClone
|
||||
|
||||
@pendingClone.setter
|
||||
def pendingClone(self, pendingImport):
|
||||
@mainPendingClone.setter
|
||||
def mainPendingClone(self, pendingImport):
|
||||
log.info("Begin clone: %s", pendingImport)
|
||||
self._pendingClone = pendingImport
|
||||
self.pointInput.setEnabled(pendingImport is not None)
|
||||
@ -196,18 +265,15 @@ class CloneTool(EditorTool):
|
||||
|
||||
def toolActive(self):
|
||||
self.editorSession.selectionTool.hideSelectionWalls = True
|
||||
if self.pendingClone is not None:
|
||||
self.pendingClone = None
|
||||
|
||||
if self.mainPendingClone is None:
|
||||
if self.editorSession.currentSelection is None:
|
||||
return
|
||||
|
||||
# This makes a reference to the latest revision in the editor.
|
||||
# If the cloned area is changed between "Clone" and "Confirm", the changed
|
||||
# blocks will be moved.
|
||||
# blocks will be cloned.
|
||||
pos = self.editorSession.currentSelection.origin
|
||||
self.originPoint = pos
|
||||
self.offsetPoint = pos
|
||||
pendingImport = PendingImport(self.editorSession.currentDimension, pos,
|
||||
self.editorSession.currentSelection,
|
||||
self.tr("<Cloned Object>"))
|
||||
@ -215,6 +281,8 @@ class CloneTool(EditorTool):
|
||||
|
||||
self.editorSession.pushCommand(moveCommand)
|
||||
|
||||
self.updateTiling()
|
||||
|
||||
def toolInactive(self):
|
||||
self.editorSession.selectionTool.hideSelectionWalls = False
|
||||
# if self.mainCloneNode:
|
||||
@ -223,31 +291,29 @@ class CloneTool(EditorTool):
|
||||
self.confirmClone()
|
||||
|
||||
def confirmClone(self):
|
||||
if self.pendingClone is None:
|
||||
if self.mainPendingClone is None:
|
||||
return
|
||||
|
||||
command = CloneFinishCommand(self, self.pendingClone)
|
||||
command = CloneFinishCommand(self, self.mainPendingClone, self.originPoint)
|
||||
|
||||
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 = []
|
||||
for pos in self.getTilingPositions():
|
||||
task = self.editorSession.currentDimension.copyBlocksIter(dim, dim.bounds, pos,
|
||||
biomes=True, create=True)
|
||||
for clone in self.pendingClones:
|
||||
# TODO don't use intermediate schematic...
|
||||
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)
|
||||
|
||||
showProgress(self.tr("Pasting..."), *tasks)
|
||||
|
||||
self.editorSession.pushCommand(command)
|
||||
self.originPoint = None
|
||||
|
||||
@property
|
||||
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
|
||||
def clonePosition(self, value):
|
||||
@ -258,6 +324,7 @@ class CloneTool(EditorTool):
|
||||
self.pointInput.point = value
|
||||
self.pointInputChanged(value)
|
||||
|
||||
|
||||
# --- Mouse events ---
|
||||
|
||||
def mouseMove(self, event):
|
||||
@ -283,3 +350,6 @@ class CloneTool(EditorTool):
|
||||
if newPoint != oldPoint:
|
||||
command = CloneOffsetCommand(self, oldPoint, newPoint)
|
||||
self.editorSession.pushCommand(command)
|
||||
|
||||
def cloneIsMoving(self, newPoint):
|
||||
self.updateTilingPositions(newPoint)
|
||||
|
@ -10,7 +10,9 @@ from mcedit2.rendering.depths import DepthOffset
|
||||
from mcedit2.rendering.scenegraph.matrix import TranslateNode, RotateNode
|
||||
from mcedit2.rendering.scenegraph.scenenode import Node
|
||||
from mcedit2.rendering.worldscene import WorldScene
|
||||
from mcedit2.util.showprogress import showProgress
|
||||
from mcedit2.util.worldloader import WorldLoader
|
||||
from mceditlib.export import extractSchematicFromIter
|
||||
from mceditlib.geometry import Vector
|
||||
from mceditlib.selection import BoundingBox
|
||||
from mceditlib.transform import SelectionTransform, DimensionTransform
|
||||
@ -156,6 +158,9 @@ class PendingImportNode(Node, QtCore.QObject):
|
||||
# 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)
|
||||
@ -166,6 +171,7 @@ class PendingImportNode(Node, QtCore.QObject):
|
||||
point = self.getBaseFromTransformed(bounds.origin)
|
||||
if self.basePosition != point:
|
||||
self.basePosition = point
|
||||
self.importIsMoving.emit(point)
|
||||
|
||||
def getBaseFromTransformed(self, point):
|
||||
offset = self.pendingImport.basePosition - self.pendingImport.importPos
|
||||
@ -226,10 +232,11 @@ class PendingImportNode(Node, QtCore.QObject):
|
||||
|
||||
@basePosition.setter
|
||||
def basePosition(self, value):
|
||||
value = Vector(*value)
|
||||
if value == self.positionTranslateNode.translateOffset:
|
||||
return
|
||||
|
||||
self.positionTranslateNode.translateOffset = Vector(*value)
|
||||
self.positionTranslateNode.translateOffset = value
|
||||
self.updateTransformedSceneOffset()
|
||||
self.updateBoxHandle()
|
||||
|
||||
@ -395,9 +402,10 @@ class PendingImport(QtCore.QObject):
|
||||
|
||||
@rotation.setter
|
||||
def rotation(self, value):
|
||||
value = tuple(value)
|
||||
if self._rotation == value:
|
||||
return
|
||||
self._rotation = Vector(*value)
|
||||
self._rotation = value
|
||||
self.updateTransform()
|
||||
self.rotationChanged.emit(self._rotation)
|
||||
|
||||
|
Reference in New Issue
Block a user