Rework brush shapes a bit

ShapedSelection -> ShapeFuncSelection
BrushShape now has createShapedSelection in addition to shapeFunc
createShapedSelection  is called instead of ShapeFuncSelection
createOptionsWidget added to BrushShape but not yet used...
Move shapeFuncs from mceditlib.selection to brush.shapes
Fix Diamond shape (recenters coordinates)
Remove Box shapeFunc - return box from createShapedSelection.
This commit is contained in:
David Vierra 2015-06-02 15:26:43 -10:00
parent a0bac1c4a3
commit 4e284e64c6
7 changed files with 121 additions and 66 deletions

View File

@ -17,7 +17,6 @@ from mcedit2.util.load_ui import load_ui, registerCustomWidget
from mcedit2.util.settings import Settings
from mcedit2.util.showprogress import showProgress
from mcedit2.util.worldloader import WorldLoader
from mceditlib.selection import ShapedSelection
from mceditlib.util import exhaust
@ -72,7 +71,8 @@ class BrushCommand(SimplePerformCommand):
yield 0, len(self.points), "Applying {0} brush...".format(self.brushMode.name)
try:
#xxx combine selections
selections = [ShapedSelection(self.brushMode.brushBoxForPoint(point, self.options), self.brushShape.shapeFunc) for point in self.points]
selections = [self.brushShape.createShapedSelection(self.brushMode.brushBoxForPoint(point, self.options))
for point in self.points]
self.brushMode.applyToSelections(self, selections)
except NotImplementedError:
for i, point in enumerate(self.points):

View File

@ -17,7 +17,7 @@ class MaskLevel(object):
"""
Dimension emulator to be used for rendering brushes and selections.
:type selection: mceditlib.selection.ShapedSelection
:type selection: mceditlib.selection.ShapeFuncSelection
:param selection:
:param fillBlock:
:param blocktypes:

View File

@ -10,7 +10,7 @@ from mcedit2.widgets.blockpicker import BlockTypeButton
from mcedit2.widgets.layout import Column, Row
from mceditlib.anvil.biome_types import BiomeTypes
from mceditlib.geometry import Vector
from mceditlib.selection import ShapedSelection, BoundingBox
from mceditlib.selection import BoundingBox
log = logging.getLogger(__name__)
@ -105,7 +105,8 @@ class Fill(BrushMode):
return self.brushBoundingBox(point, options)
def createCursorLevel(self, brushTool):
selection = ShapedSelection(self.brushBoxForPoint((0, 0, 0), brushTool.options), brushTool.brushShape.shapeFunc)
box = self.brushBoxForPoint((0, 0, 0), brushTool.options)
selection = brushTool.brushShape.createShapedSelection(box)
cursorLevel = MaskLevel(selection,
self.blockTypeButton.block,
brushTool.editorSession.worldEditor.blocktypes)
@ -157,8 +158,8 @@ class Biome(BrushMode):
def createCursorLevel(self, brushTool):
box = self.brushBoxForPoint((0, 0, 0), brushTool.options)
selection = brushTool.brushShape.createShapedSelection(box)
selection = ShapedSelection(box, brushTool.brushShape.shapeFunc)
cursorLevel = MaskLevel(selection,
brushTool.editorSession.worldEditor.blocktypes["minecraft:grass"],
brushTool.editorSession.worldEditor.blocktypes,

View File

@ -3,6 +3,7 @@
"""
from __future__ import absolute_import, division, print_function, unicode_literals
import logging
import numpy
from mceditlib import selection
log = logging.getLogger(__name__)
@ -11,24 +12,127 @@ log = logging.getLogger(__name__)
class BrushShape(object):
ID = NotImplemented
icon = NotImplemented
shapeFunc = NotImplemented
def createShapedSelection(self, box):
"""
Return a SelectionBox that selects the blocks inside this shape.
The default implementation returns a ShapeFuncSelection using self.shapeFunc. Subclasses
may override this to return different types of SelectionBox.
TODO: BitmapSelectionBox
:param box: Bounding box of the selection
:type box: BoundingBox
:return: SelectionBox object that selects all blocks inside this shape
:rtype: SelectionBox
"""
return selection.ShapeFuncSelection(box, self.shapeFunc)
def shapeFunc(self, blockPositions, selectionSize):
"""
Return a 3D boolean array for the blocks selected by this shape in a requested area.
(Note that numpy arrays have a 'shape' attribute which gives the length of the array along
each dimension. Sorry for the confusion.)
The coordinates of the blocks are given by `blockPositions`, which is a 4D array where
the first axis has size 3 and represents the Y, Z, and X coordinates. The coordinates
given are relative to the bounding box for this shape. The remaining 3 axes have the shape
of the requested area.
`blockPositions` may be separated into coordinate arrays by writing
`y, z, x = blockPositions`.
The size and shape of the array to return is given by the shapes of the arrays in
`blockPositions`.
The size of the shape's bounding box is given by selectionSize.
:param blockPositions: Coordinates of requested blocks relative to Shape's bounding box.
:type blockPositions: numpy.ndarray[ndims=4,dtype=float32]
:param selectionSize: Size of the Shape's bounding box.
:type selectionSize: (int, int, int)
:return: Boolean array of the same shape as blockPositions[0] where selected blocks are True
:rtype: numpy.ndarray[ndims=3,dtype=bool]
"""
raise NotImplementedError
def createOptionsWidget(self):
"""
Return a QWidget to present additional options for this shape.
If there are no options to present, return None. This is the default implementation.
:return: Options widget
:rtype: QWidget | NoneType
"""
return None
class Round(BrushShape):
ID = "Round"
icon = "shapes/round.png"
shapeFunc = staticmethod(selection.SphereShape)
def shapeFunc(self, blockPositions, shape):
# For spheres: x^2 + y^2 + z^2 <= r^2
# For ovoids: x^2/rx^2 + y^2/ry^2 + z^2/rz^2 <= 1
#
# blockPositions are the positions of the lower left corners of each block.
#
# to define the sphere, we measure the distance from the center of each block
# to the sphere's center, which will be on a block edge when the size is even
# or at a block center when the size is odd.
#
# to this end, we offset blockPositions downward so the sphere's center is at 0, 0, 0
# and blockPositions are the positions of the centers of the blocks
radius = shape / 2.0
offset = radius - 0.5
blockPositions -= offset[:, None, None, None]
blockPositions *= blockPositions
radius2 = radius * radius
blockPositions /= radius2[:, None, None, None]
distances = sum(blockPositions, 0)
return distances <= 1
class Square(BrushShape):
ID = "Square"
icon = "shapes/square.png"
shapeFunc = staticmethod(selection.BoxShape)
def createShapedSelection(self, box):
# BoundingBox is already a SelectionBox, so just return it
return box
class Diamond(BrushShape):
ID = "Diamond"
icon = "shapes/diamond.png"
shapeFunc = staticmethod(selection.DiamondShape)
def shapeFunc(self, blockPositions, selectionSize):
# This is an octahedron.
# Inscribed in a cube: |x| + |y| + |z| <= cubeSize
# Inscribed in a box: |x/w| + |y/h| + |z/l| <= 1
selectionSize /= 2
# Recenter coordinates
blockPositions -= (selectionSize - 0.5)[:, None, None, None]
# Distances should be positive
blockPositions = numpy.abs(blockPositions)
# Divide by w, h, l
blockPositions /= selectionSize[:, None, None, None]
# Add x, y, z together
distances = numpy.sum(blockPositions, 0)
return distances <= 1
class Cylinder(BrushShape):
ID = "Cylinder"

View File

@ -62,10 +62,10 @@ class SelectionCoordinateWidget(QtGui.QWidget):
self.yMaxInput.setMinimum(minVal)
self.zMaxInput.setMinimum(minVal)
boxChanged = QtCore.Signal(BoundingBox)
_boundingBox = BoundingBox()
@property
def boundingBox(self):
return self._boundingBox
@ -88,7 +88,6 @@ class SelectionCoordinateWidget(QtGui.QWidget):
self.yMaxInput.setValue(box.maxy)
self.zMaxInput.setValue(box.maxz)
def setMinX(self, value):
origin, size = self.boundingBox
origin = value, origin[1], origin[2]
@ -96,7 +95,6 @@ class SelectionCoordinateWidget(QtGui.QWidget):
self.boundingBox = box
self.boxChanged.emit(box)
def setMinY(self, value):
origin, size = self.boundingBox
origin = origin[0], value, origin[2]
@ -275,18 +273,13 @@ class SelectionTool(EditorTool):
def mouseRelease(self, event):
self.boxHandleNode.mouseRelease(event)
selectionColor = (0.8, 0.8, 1.0)
alpha = 0.33
showPreviousSelection = True
def createShapedSelection(self, box):
if self.shapeInput.currentShape is shapes.Square:
return box # ugly hack
else:
return selection.ShapedSelection(box, self.shapeInput.currentShape.shapeFunc)
return self.shapeInput.currentShape.createShapedSelection(box)
class SelectionCursorRenderNode(rendergraph.RenderNode):
def drawSelf(self):

View File

@ -6,14 +6,14 @@ import logging
import timeit
from PySide import QtGui
from mcedit2.rendering.selection import SelectionScene
from mceditlib.selection import ShapedSelection, SphereShape
from mceditlib.selection import ShapeFuncSelection, SphereShape
from mceditlib.selection import BoundingBox
log = logging.getLogger(__name__)
def main():
app = QtGui.QApplication([])
selection = ShapedSelection(BoundingBox((0, 0, 0), (63, 63, 63)), SphereShape)
selection = ShapeFuncSelection(BoundingBox((0, 0, 0), (63, 63, 63)), SphereShape)
scene = SelectionScene()
def timeBuild():
scene.selection = selection

View File

@ -526,7 +526,6 @@ class BoundingBox(SelectionBox):
"""The largest chunk position contained in this box"""
return ((self.origin.z + self.size.z - 1) >> 4) + 1
@property
def isChunkAligned(self):
return (self.origin.x & 0xf == 0) and (self.origin.z & 0xf == 0)
@ -537,7 +536,7 @@ class FloatBox(BoundingBox):
type = float
class ShapedSelection(BoundingBox):
class ShapeFuncSelection(BoundingBox):
def __init__(self, box, shapeFunc):
"""
Generic class for implementing shaped selections via a shapeFunc callable.
@ -562,7 +561,7 @@ class ShapedSelection(BoundingBox):
:type shapeFunc: Callable(blockPositions, selectionShape)
:type box: BoundingBox
"""
super(ShapedSelection, self).__init__(box.origin, box.size)
super(ShapeFuncSelection, self).__init__(box.origin, box.size)
self.shapeFunc = shapeFunc
def box_mask(self, box):
@ -611,45 +610,3 @@ class ShapedSelection(BoundingBox):
yield x[i], y[i], z[i]
# --- Shape functions ---
def SphereShape(blockPositions, shape):
# x^2 + y^2 + z^2 < r^2
#
# blockPositions are the positions of the lower left corners of each block.
#
# to define the sphere, we measure the distance from the center of each block
# to the sphere's center, which will be on a block edge when the size is even
# or at a block center when the size is odd.
#
# to this end, we offset blockPositions downward so the sphere's center is at 0, 0, 0
# and blockPositions are the positions of the centers of the blocks
radius = shape / 2.0
offset = radius - 0.5
blockPositions -= offset[:, None, None, None]
blockPositions *= blockPositions
radius2 = radius * radius
blockPositions /= radius2[:, None, None, None]
distances = sum(blockPositions, 0)
return distances < 1
def BoxShape(blockPositions, shape):
blockPositions /= shape[:, None, None, None] # XXXXXX USING DIVIDE FOR A RECTANGLE
distances = numpy.absolute(blockPositions).max(0)
return distances < .5
def DiamondShape(blockPositions, shape):
blockPositions = numpy.abs(blockPositions)
shape /= 2
blockPositions /= shape[:, None, None, None]
distances = numpy.sum(blockPositions, 0)
return distances < 1