Add scale/flip options to Clone and Move

This commit is contained in:
David Vierra 2017-03-30 17:39:25 -10:00
parent 204045a3ef
commit ea32ca86c3
8 changed files with 436 additions and 40 deletions

View File

@ -15,6 +15,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 mcedit2.widgets.scale_widget import ScaleWidget
from mceditlib import transform
log = logging.getLogger(__name__)
@ -69,6 +70,21 @@ class CloneRotateCommand(QtGui.QUndoCommand):
self.cloneTool.setRotation(self.newRotation)
class CloneScaleCommand(QtGui.QUndoCommand):
def __init__(self, oldScale, newScale, cloneTool):
super(CloneScaleCommand, self).__init__()
self.cloneTool = cloneTool
self.setText(QtGui.qApp.tr("Scale Cloned Objects"))
self.newScale = newScale
self.oldScale = oldScale
def undo(self):
self.cloneTool.setScale(self.oldScale)
def redo(self):
self.cloneTool.setScale(self.newScale)
class CloneFinishCommand(SimpleRevisionCommand):
def __init__(self, cloneTool, pendingImport, originPoint, *args, **kwargs):
super(CloneFinishCommand, self).__init__(cloneTool.editorSession, cloneTool.tr("Finish Clone"), *args, **kwargs)
@ -130,6 +146,9 @@ class CloneTool(EditorTool):
self.rotationInput = RotationWidget()
self.rotationInput.rotationChanged.connect(self.rotationChanged)
self.scaleInput = ScaleWidget()
self.scaleInput.scaleChanged.connect(self.scaleChanged)
confirmButton = QtGui.QPushButton(self.tr("Confirm")) # xxxx should be in worldview
confirmButton.clicked.connect(self.confirmClone)
@ -147,6 +166,7 @@ class CloneTool(EditorTool):
self.rotationInput,
Row(self.rotateRepeatsCheckbox,
self.rotateOffsetCheckbox),
self.scaleInput,
Row(QtGui.QLabel(self.tr("Repeat count: ")), self.repeatCountInput),
confirmButton,
None))
@ -159,9 +179,11 @@ class CloneTool(EditorTool):
self.updateTiling()
def rotationChanged(self, rots, live):
scale = self.scaleInput.scale
if live:
for node, (nodePos, nodeRots) in zip(self.pendingCloneNodes, self.getTilingPositions(rotations=rots)):
for node, (nodePos, nodeRots, nodeScale) in zip(self.pendingCloneNodes, self.getTilingPositions(None, rots, scale)):
node.setPreviewRotation(nodeRots)
node.setPreviewScale(nodeScale)
node.setPreviewBasePosition(nodePos + node.pendingImport.transformOffset)
self.editorSession.updateView()
else:
@ -170,6 +192,20 @@ class CloneTool(EditorTool):
self.editorSession.pushCommand(command)
self.updateTiling()
def scaleChanged(self, scale, live):
rots = self.rotationInput.rotation
if live:
for node, (nodePos, nodeRots, nodeScale) in zip(self.pendingCloneNodes, self.getTilingPositions(None, rots, scale)):
node.setPreviewRotation(nodeRots)
node.setPreviewScale(nodeScale)
node.setPreviewBasePosition(nodePos + node.pendingImport.transformOffset)
self.editorSession.updateView()
else:
if self.mainPendingClone and self.mainPendingClone.scale != scale:
command = CloneScaleCommand(self.mainPendingClone.scale, scale, self)
self.editorSession.pushCommand(command)
self.updateTiling()
def setRepeatCount(self, value):
self.repeatCount = value
self.updateTiling()
@ -181,6 +217,13 @@ class CloneTool(EditorTool):
self.mainPendingClone.rotation = rots
self.updateTiling()
def setScale(self, scale):
if self.mainPendingClone is None:
return
else:
self.mainPendingClone.scale = scale
self.updateTiling()
def updateTiling(self):
if self.mainPendingClone is None:
repeatCount = 0
@ -221,21 +264,24 @@ class CloneTool(EditorTool):
def updateTilingPositions(self, offsetPoint=None):
if self.originPoint is not None:
for clone, (pos, rots) in zip(self.pendingClones, self.getTilingPositions(offsetPoint)):
for clone, (pos, rots, scale) in zip(self.pendingClones, self.getTilingPositions(offsetPoint)):
clone.basePosition = pos
clone.rotation = rots
clone.scale = scale
self.editorSession.updateView()
def getTilingPositions(self, offsetPoint=None, rotations=None):
def getTilingPositions(self, offsetPoint=None, rotations=None, scale=None):
rotateRepeats = self.rotateRepeatsCheckbox.isChecked()
rotateOffsets = self.rotateOffsetCheckbox.isChecked()
baseRotations = rotations or self.mainPendingClone.rotation
rotations = baseRotations
scale = scale or self.mainPendingClone.scale
matrix = transform.rotationMatrix((0, 0, 0), *rotations)
matrix = transform.transformationMatrix((0, 0, 0), rotations, scale)
matrix = numpy.linalg.inv(matrix)[:3, :3]
# TODO: Use scales here
if offsetPoint is None:
offsetPoint = self.mainPendingClone.basePosition
if None not in (offsetPoint, self.originPoint):
@ -243,7 +289,7 @@ class CloneTool(EditorTool):
offset = offsetPoint - self.originPoint
for i in range(self.repeatCount):
pos = pos + offset
yield pos.intfloor(), rotations
yield pos.intfloor(), rotations, scale
if rotateRepeats:
rotations = [a+b for a,b in zip(rotations, baseRotations)]
if rotateOffsets:

View File

@ -15,6 +15,7 @@ from mcedit2.util.showprogress import showProgress
from mcedit2.widgets.coord_widget import CoordinateWidget
from mcedit2.widgets.layout import Column
from mcedit2.widgets.rotation_widget import RotationWidget
from mcedit2.widgets.scale_widget import ScaleWidget
from mceditlib.export import extractSchematicFromIter
from mceditlib.selection import BoundingBox
@ -72,6 +73,21 @@ class MoveRotateCommand(QtGui.QUndoCommand):
self.pendingImport.rotation = self.newRotation
class MoveScaleCommand(QtGui.QUndoCommand):
def __init__(self, oldScale, newScale, pendingImport):
super(MoveScaleCommand, self).__init__()
self.pendingImport = pendingImport
self.setText(QtGui.qApp.tr("Scale Object"))
self.newScale = newScale
self.oldScale = oldScale
def undo(self):
self.pendingImport.scale = self.oldScale
def redo(self):
self.pendingImport.scale = self.newScale
class MoveFinishCommand(SimpleRevisionCommand):
def __init__(self, moveTool, pendingImport, *args, **kwargs):
super(MoveFinishCommand, self).__init__(moveTool.editorSession, moveTool.tr("Finish Move"), *args, **kwargs)
@ -114,6 +130,9 @@ class MoveTool(EditorTool):
self.rotationInput = RotationWidget()
self.rotationInput.rotationChanged.connect(self.rotationChanged)
self.scaleInput = ScaleWidget()
self.scaleInput.scaleChanged.connect(self.scaleChanged)
self.copyOptionsWidget = QtGui.QGroupBox(self.tr("Options"))
self.copyAirCheckbox = QtGui.QCheckBox(self.tr("Copy Air"))
@ -123,6 +142,7 @@ class MoveTool(EditorTool):
confirmButton.clicked.connect(self.confirmImport)
self.toolWidget.setLayout(Column(self.pointInput,
self.rotationInput,
self.scaleInput,
self.copyOptionsWidget,
confirmButton,
None))
@ -137,6 +157,16 @@ class MoveTool(EditorTool):
self.editorSession.updateView()
def scaleChanged(self, scale, live):
if self.currentImport:
if live:
self.currentImportNode.setPreviewScale(scale)
elif scale != self.currentImport.scale:
command = MoveScaleCommand(self.currentImport.scale, scale, self.currentImport)
self.editorSession.pushCommand(command)
self.editorSession.updateView()
def pointInputChanged(self, value):
if value is not None:
self.importDidMove(value, self.currentImport.basePosition)
@ -161,6 +191,7 @@ class MoveTool(EditorTool):
self._currentImport = pendingImport
self.pointInput.setEnabled(pendingImport is not None)
if pendingImport is not None:
pendingImport.scaleChanged.connect(self.setScaleInput)
pendingImport.rotationChanged.connect(self.setRotationInput)
pendingImport.positionChanged.connect(self.setPositionInput)
@ -184,6 +215,9 @@ class MoveTool(EditorTool):
def currentImportNode(self):
return self._currentImportNode
def setScaleInput(self, scale):
self.scaleInput.scale = scale
def setRotationInput(self, rots):
self.rotationInput.rotation = rots

View File

@ -7,7 +7,7 @@ from PySide import QtCore, QtGui
from PySide.QtCore import Qt
from mcedit2.handles.boxhandle import BoxHandle
from mcedit2.rendering.depths import DepthOffsets
from mcedit2.rendering.scenegraph.matrix import Translate, Rotate
from mcedit2.rendering.scenegraph.matrix import Translate, Rotate, Scale
from mcedit2.rendering.scenegraph.scenenode import Node
from mcedit2.rendering.selection import SelectionBoxNode
from mcedit2.rendering.worldscene import WorldScene
@ -123,6 +123,10 @@ class PendingImportNode(Node, QtCore.QObject):
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.
@ -130,7 +134,7 @@ class PendingImportNode(Node, QtCore.QObject):
self.plainSceneNode = Node("plainScene")
self.positionTranslate = Translate()
self.plainSceneNode.addState(self.positionTranslate)
self.plainSceneNode.addChild(self.rotateNode)
self.plainSceneNode.addChild(self.scaleNode)
self.addChild(self.plainSceneNode)
@ -178,6 +182,7 @@ class PendingImportNode(Node, QtCore.QObject):
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).
@ -193,6 +198,7 @@ class PendingImportNode(Node, QtCore.QObject):
self.importMoved.emit(point, oldPoint)
def handleBoundsChanged(self, bounds):
log.info("handleBoundsChanged: %s", bounds)
self.setPreviewBasePosition(bounds.origin)
def setPreviewBasePosition(self, origin):
@ -214,6 +220,16 @@ class PendingImportNode(Node, QtCore.QObject):
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")
@ -458,23 +474,23 @@ class PendingImport(QtCore.QObject):
@property
def scale(self):
return self._rotation
return self._scale
@scale.setter
def scale(self, value):
if self._scale == value:
return
self._scale = Vector(*value)
self.scaleChanged.emit(self._scale)
self.updateTransform()
self.scaleChanged.emit(self._scale)
def updateTransform(self):
if self.rotation == (0, 0, 0) and self.scale == (0, 0, 0):
if self.rotation == (0, 0, 0) and self.scale == (1, 1, 1):
self.transformedDim = None
self.transformOffset = Vector(0, 0, 0)
else:
selectionDim = SelectionTransform(self.sourceDim, self.selection)
self.transformedDim = DimensionTransform(selectionDim, self.rotateAnchor, *self.rotation)
self.transformedDim = DimensionTransform(selectionDim, self.rotateAnchor, self.rotation, self.scale)
self.transformOffset = self.transformedDim.bounds.origin - self.selection.origin
self.updateImportPos()
@ -512,3 +528,22 @@ class Rotate3DNode(Node):
def setAnchor(self, point):
self.anchor.translateOffset = point
self.recenter.translateOffset = -point
class Scale3DNode(Node):
def __init__(self):
super(Scale3DNode, self).__init__()
self.anchor = Translate()
self.scale = Scale()
self.recenter = Translate()
self.addState(self.anchor)
self.addState(self.scale)
self.addState(self.recenter)
def setScale(self, scale):
self.scale.scale = scale
def setAnchor(self, point):
self.anchor.translateOffset = point
self.recenter.translateOffset = -point

View File

@ -90,10 +90,21 @@ class Scale(states.SceneNodeState):
GL.glMatrixMode(GL.GL_MODELVIEW)
GL.glPopMatrix()
def __init__(self, scale):
def __init__(self, scale=(1.0, 1.0, 1.0)):
super(Scale, self).__init__()
self.scale = scale
_scale = (1.0, 1.0, 1.0)
@property
def scale(self):
return self._scale
@scale.setter
def scale(self, value):
self._scale = value
self.dirty = True
class MatrixState(states.SceneNodeState):
def enter(self):

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>scaleWidget</class>
<widget class="QWidget" name="scaleWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>455</width>
<height>338</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Scale X:</string>
</property>
</widget>
</item>
<item>
<widget class="ScaleSpinSlider" name="xScaleSpinSlider" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="xFlipButton">
<property name="text">
<string>Flip</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Scale Y:</string>
</property>
</widget>
</item>
<item>
<widget class="ScaleSpinSlider" name="yScaleSpinSlider" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="yFlipButton">
<property name="text">
<string>Flip</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Scale Z:</string>
</property>
</widget>
</item>
<item>
<widget class="ScaleSpinSlider" name="zScaleSpinSlider" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="zFlipButton">
<property name="text">
<string>Flip</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ScaleSpinSlider</class>
<extends>QWidget</extends>
<header>mcedit2/widgets/spinslider.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,92 @@
"""
rotation_widget
"""
from __future__ import absolute_import, division, print_function, unicode_literals
import logging
from PySide import QtGui, QtCore
from mcedit2.ui.scale_widget import Ui_scaleWidget
from mcedit2.util.resources import resourcePath
log = logging.getLogger(__name__)
class ScaleWidget(QtGui.QWidget, Ui_scaleWidget):
def __init__(self):
super(ScaleWidget, self).__init__()
self.setupUi(self)
self.xScaleSpinSlider.valueChanged.connect(self.setXScale)
self.yScaleSpinSlider.valueChanged.connect(self.setYScale)
self.zScaleSpinSlider.valueChanged.connect(self.setZScale)
icon = QtGui.QIcon(resourcePath("mcedit2/assets/mcedit2/icons/mirror.png"))
self.xFlipButton.setIcon(icon)
self.yFlipButton.setIcon(icon)
self.zFlipButton.setIcon(icon)
self.xFlipButton.clicked.connect(self.xFlipClicked)
self.yFlipButton.clicked.connect(self.yFlipClicked)
self.zFlipButton.clicked.connect(self.zFlipClicked)
self.xScale = self.yScale = self.zScale = 1.0
def xFlipClicked(self):
x, y, z = self.scale
self.scale = -x, y, z
def yFlipClicked(self):
x, y, z = self.scale
self.scale = x, -y, z
def zFlipClicked(self):
x, y, z = self.scale
self.scale = x, y, -z
scaleChanged = QtCore.Signal(object, bool)
@property
def scale(self):
return self.xScale, self.yScale, self.zScale
@scale.setter
def scale(self, value):
if value == self.scale:
return
xScale, yScale, zScale = value
self.xScale, self.yScale, self.zScale = value
self.xScaleSpinSlider.setValue(xScale)
self.yScaleSpinSlider.setValue(yScale)
self.zScaleSpinSlider.setValue(zScale)
self.emitScaleChanged(False)
def emitScaleChanged(self, live):
log.info("emitScaleChanged %s %s", self.scale, live)
self.scaleChanged.emit(self.scale, live)
def setXScale(self, value, live):
log.info("setXScale %s %s", value, live)
if self.xScale == value and live:
return
self.xScale = value
self.emitScaleChanged(live)
def setYScale(self, value, live):
if self.yScale == value and live:
return
self.yScale = value
self.emitScaleChanged(live)
def setZScale(self, value, live):
if self.zScale == value and live:
return
self.zScale = value
self.emitScaleChanged(live)

View File

@ -116,16 +116,49 @@ class SpinSlider(QtGui.QWidget):
def setMinimum(self, value):
self._minimum = value
self.slider.setMinimum(value * self.sliderFactor)
self.spinBox.setMinimum(value)
if value is not None:
value = self.toSlider(value)
self.slider.setMinimum(value)
def maximum(self):
return self._maximum
def setMaximum(self, value):
self._maximum = value
self.slider.setMaximum(value * self.sliderFactor)
self.spinBox.setMaximum(value)
if value is not None:
value = self.toSlider(value)
self.slider.setMaximum(value)
valueChanged = QtCore.Signal(float, bool)
@registerCustomWidget
class DoubleSpinSlider(SpinSlider):
def __init__(self, *a, **kw):
kw['double'] = True
super(DoubleSpinSlider, self).__init__(*a, **kw)
@registerCustomWidget
class ScaleSpinSlider(DoubleSpinSlider):
def __init__(self, *a, **kw):
kw['minimum'] = -20
kw['maximum'] = 20
kw['value'] = 1
super(ScaleSpinSlider, self).__init__(*a, **kw)
def toSlider(self, value):
if value < -1.0:
return (value * 50) - 1000
if value > 1.0:
return (value * 50) + 1000
return value * 1000
def fromSlider(self, value):
if value < -1000:
return ((value + 1000) * 2) / 100.
if value > 1000:
return ((value - 1000) * 2) / 100.
return value / 1000.

View File

@ -31,33 +31,41 @@ def transformBounds(bounds, matrix):
corners = np.hstack([corners, ([1],)*8])
corners = corners * matrix
minx = min(corners[:, 0])
miny = min(corners[:, 1])
minz = min(corners[:, 2])
maxx = max(corners[:, 0])
maxy = max(corners[:, 1])
maxz = max(corners[:, 2])
minx = math.floor(min(corners[:, 0]))
miny = math.floor(min(corners[:, 1]))
minz = math.floor(min(corners[:, 2]))
maxx = math.ceil(max(corners[:, 0]))
maxy = math.ceil(max(corners[:, 1]))
maxz = math.ceil(max(corners[:, 2]))
if maxx % 1:
maxx += 1
if maxy % 1:
maxy += 1
if maxz % 1:
maxz += 1
# Why? Weird hacks for rotation?
# if maxx % 1:
# maxx += 1
# if maxy % 1:
# maxy += 1
# if maxz % 1:
# maxz += 1
newbox = BoundingBox(origin=Vector(minx, miny, minz).intfloor(),
maximum=Vector(maxx, maxy, maxz).intfloor())
return newbox
def rotationMatrix(anchor, rotX, rotY, rotZ):
def transformationMatrix(anchor, rotation, scale):
rotX, rotY, rotZ = rotation
scaleInv = tuple([1.0/c if c != 0 else 1.0 for c in scale])
translate = np.matrix(np.identity(4))
translate[3, 0] = anchor[0]
translate[3, 1] = anchor[1]
translate[3, 2] = anchor[2]
# Rotate around center of cells.
anchor = Vector(*anchor) - (0.5, 0.5, 0.5)
scaleMatrix = np.matrix(np.identity(4))
scaleMatrix[0, 0] = scaleInv[0]
scaleMatrix[1, 1] = scaleInv[1]
scaleMatrix[2, 2] = scaleInv[2]
reverse_translate = np.matrix(np.identity(4))
reverse_translate[3, 0] = -anchor[0]
@ -73,6 +81,8 @@ def rotationMatrix(anchor, rotX, rotY, rotZ):
if rotZ:
matrix = npRotate('z', rotZ) * matrix
matrix = scaleMatrix * matrix
matrix = reverse_translate * matrix
return matrix
@ -250,8 +260,9 @@ class SelectionTransform(DimensionTransformBase):
section.Blocks[sectionMask] = baseSection.Blocks[sectionMask]
section.Data[sectionMask] = baseSection.Data[sectionMask]
class DimensionTransform(DimensionTransformBase):
def __init__(self, dimension, anchor, rotX=0, rotY=0, rotZ=0):
def __init__(self, dimension, anchor, rotation=(0, 0, 0), scale=(1, 1, 1)):
"""
A wrapper around a WorldEditorDimension that applies a three-dimensional rotation
around a given anchor point. The wrapped dimension's bounds will be different from the
@ -263,15 +274,17 @@ class DimensionTransform(DimensionTransformBase):
dimension: mceditlib.worldeditor.WorldEditorDimension
The dimension to wrap and apply rotations to
anchor: Vector
The point to rotate the dimension around
anchor: mceditlib.geometry.Vector
The point to rotate and scale the dimension around
rotX: float
rotY: float
rotZ: float
rotation: float[3]
The angles to rotate the dimension around, along each axis respectively.
The angles are given in radians.
scale: float[3]
The scales to resize the dimension along each axis respectively. 1.0 is
normal size.
Returns
-------
@ -279,12 +292,14 @@ class DimensionTransform(DimensionTransformBase):
A dimension that acts as a rotated version of the given dimension.
"""
super(DimensionTransform, self).__init__(dimension)
rotX, rotY, rotZ = rotation
self.rotX = rotX
self.rotY = rotY
self.rotZ = rotZ
self.anchor = anchor
self.scale = scale
self.matrix = rotationMatrix(anchor, rotX, rotY, rotZ)
self.matrix = transformationMatrix(anchor, rotation, scale)
blockRotation = BlockRotations(dimension.blocktypes)
rotationTable = blankRotationTable()
@ -309,6 +324,10 @@ class DimensionTransform(DimensionTransformBase):
self._transformedBounds = transformBounds(dimension.bounds, self.matrix)
print("Bounds: ", dimension.bounds)
print("Transformed: ", self._transformedBounds)
print("Anchor: ", anchor)
def initSection(self, section):
shape = (16, 16, 16)