From 4e284e64c66d8d56c9c71e4e07746978d9d51400 Mon Sep 17 00:00:00 2001 From: David Vierra Date: Tue, 2 Jun 2015 15:26:43 -1000 Subject: [PATCH] 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. --- src/mcedit2/editortools/brush/__init__.py | 4 +- src/mcedit2/editortools/brush/masklevel.py | 2 +- src/mcedit2/editortools/brush/modes.py | 7 +- src/mcedit2/editortools/brush/shapes.py | 112 ++++++++++++++++++++- src/mcedit2/editortools/select.py | 11 +- src/mcedit2/test/time_selectionrender.py | 4 +- src/mceditlib/selection/__init__.py | 47 +-------- 7 files changed, 121 insertions(+), 66 deletions(-) diff --git a/src/mcedit2/editortools/brush/__init__.py b/src/mcedit2/editortools/brush/__init__.py index 3c9980f..4760c10 100644 --- a/src/mcedit2/editortools/brush/__init__.py +++ b/src/mcedit2/editortools/brush/__init__.py @@ -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): diff --git a/src/mcedit2/editortools/brush/masklevel.py b/src/mcedit2/editortools/brush/masklevel.py index 1018fc0..dcb0241 100644 --- a/src/mcedit2/editortools/brush/masklevel.py +++ b/src/mcedit2/editortools/brush/masklevel.py @@ -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: diff --git a/src/mcedit2/editortools/brush/modes.py b/src/mcedit2/editortools/brush/modes.py index 199cc6f..77ec43c 100644 --- a/src/mcedit2/editortools/brush/modes.py +++ b/src/mcedit2/editortools/brush/modes.py @@ -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, diff --git a/src/mcedit2/editortools/brush/shapes.py b/src/mcedit2/editortools/brush/shapes.py index 5a303f2..27d4459 100644 --- a/src/mcedit2/editortools/brush/shapes.py +++ b/src/mcedit2/editortools/brush/shapes.py @@ -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" diff --git a/src/mcedit2/editortools/select.py b/src/mcedit2/editortools/select.py index 5d735f3..1fb5a33 100644 --- a/src/mcedit2/editortools/select.py +++ b/src/mcedit2/editortools/select.py @@ -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): diff --git a/src/mcedit2/test/time_selectionrender.py b/src/mcedit2/test/time_selectionrender.py index c447c15..ec2a864 100644 --- a/src/mcedit2/test/time_selectionrender.py +++ b/src/mcedit2/test/time_selectionrender.py @@ -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 diff --git a/src/mceditlib/selection/__init__.py b/src/mceditlib/selection/__init__.py index a2804dd..d95fc5a 100644 --- a/src/mceditlib/selection/__init__.py +++ b/src/mceditlib/selection/__init__.py @@ -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 -