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:
parent
a0bac1c4a3
commit
4e284e64c6
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user