This repository has been archived on 2024-06-13. You can view files and clone it, but cannot push or open issues or pull requests.
mcedit/editortools/brush.py
David Vierra 963d2a728c Added: "Record Undo" option.
"Record Undo" is reset when loading or saving a level.
Long undo operations can be canceled.
2013-03-13 23:13:54 -10:00

1164 lines
41 KiB
Python

"""Copyright (c) 2010-2012 David Rio Vierra
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE."""
import collections
from datetime import datetime
import numpy
from numpy import newaxis
import pygame
from albow import AttrRef, Button, ValueDisplay, Row, Label, ValueButton, Column, IntField, CheckBox, FloatField, alert
import bresenham
from editortools.blockpicker import BlockPicker
from editortools.blockview import BlockButton
from editortools.editortool import EditorTool
from editortools.tooloptions import ToolOptions
from glbackground import Panel
from glutils import gl
import mcplatform
from pymclevel import block_fill, BoundingBox
import pymclevel
from pymclevel.level import extractHeights
from mceutils import ChoiceButton, CheckBoxLabel, showProgress, IntInputRow, alertException, drawTerrainCuttingWire
from os.path import basename
import tempfile
import itertools
import logging
from operation import Operation, mkundotemp
from pymclevel.mclevelbase import exhaust
from OpenGL import GL
log = logging.getLogger(__name__)
import config
BrushSettings = config.Settings("Brush")
BrushSettings.brushSizeL = BrushSettings("Brush Shape L", 3)
BrushSettings.brushSizeH = BrushSettings("Brush Shape H", 3)
BrushSettings.brushSizeW = BrushSettings("Brush Shape W", 3)
BrushSettings.updateBrushOffset = BrushSettings("Update Brush Offset", False)
BrushSettings.chooseBlockImmediately = BrushSettings("Choose Block Immediately", False)
BrushSettings.alpha = BrushSettings("Alpha", 0.66)
class BrushMode(object):
options = []
def brushBoxForPointAndOptions(self, point, options={}):
# Return a box of size options['brushSize'] centered around point.
# also used to position the preview reticle
size = options['brushSize']
origin = map(lambda x, s: x - (s >> 1), point, size)
return BoundingBox(origin, size)
def apply(self, op, point):
"""
Called by BrushOperation for brush modes that can't be implemented using applyToChunk
"""
pass
apply = NotImplemented
def applyToChunk(self, op, chunk, point):
"""
Called by BrushOperation to apply this brush mode to the given chunk with a brush centered on point.
Default implementation will compute:
brushBox: a BoundingBox for the world area affected by this brush,
brushBoxThisChunk: a box for the portion of this chunk affected by this brush,
slices: a tuple of slices that can index the chunk's Blocks array to select the affected area.
These three parameters are passed to applyToChunkSlices along with the chunk and the brush operation.
Brush modes must implement either applyToChunk or applyToChunkSlices
"""
brushBox = self.brushBoxForPointAndOptions(point, op.options)
brushBoxThisChunk, slices = chunk.getChunkSlicesForBox(brushBox)
if brushBoxThisChunk.volume == 0: return
return self.applyToChunkSlices(op, chunk, slices, brushBox, brushBoxThisChunk)
def applyToChunkSlices(self, op, chunk, slices, brushBox, brushBoxThisChunk):
raise NotImplementedError
def createOptions(self, panel, tool):
pass
class Modes:
class Fill(BrushMode):
name = "Fill"
def createOptions(self, panel, tool):
col = [
panel.modeStyleGrid,
panel.hollowRow,
panel.noiseInput,
panel.brushSizeRows,
panel.blockButton,
]
return col
def applyToChunkSlices(self, op, chunk, slices, brushBox, brushBoxThisChunk):
brushMask = createBrushMask(op.brushSize, op.brushStyle, brushBox.origin, brushBoxThisChunk, op.noise, op.hollow)
chunk.Blocks[slices][brushMask] = op.blockInfo.ID
chunk.Data[slices][brushMask] = op.blockInfo.blockData
class FloodFill(BrushMode):
name = "Flood Fill"
options = ['indiscriminate']
def createOptions(self, panel, tool):
col = [
panel.brushModeRow,
panel.blockButton
]
indiscriminateButton = CheckBoxLabel("Indiscriminate", ref=AttrRef(tool, 'indiscriminate'))
col.append(indiscriminateButton)
return col
def apply(self, op, point):
undoLevel = pymclevel.MCInfdevOldLevel(mkundotemp(), create=True)
dirtyChunks = set()
def saveUndoChunk(cx, cz):
if (cx, cz) in dirtyChunks:
return
dirtyChunks.add((cx, cz))
undoLevel.copyChunkFrom(op.level, cx, cz)
doomedBlock = op.level.blockAt(*point)
doomedBlockData = op.level.blockDataAt(*point)
checkData = (doomedBlock not in (8, 9, 10, 11))
indiscriminate = op.options['indiscriminate']
if doomedBlock == op.blockInfo.ID:
return
if indiscriminate:
checkData = False
if doomedBlock == 2: # grass
doomedBlock = 3 # dirt
x, y, z = point
saveUndoChunk(x // 16, z // 16)
op.level.setBlockAt(x, y, z, op.blockInfo.ID)
op.level.setBlockDataAt(x, y, z, op.blockInfo.blockData)
def processCoords(coords):
newcoords = collections.deque()
for (x, y, z) in coords:
for _dir, offsets in pymclevel.faceDirections:
dx, dy, dz = offsets
p = (x + dx, y + dy, z + dz)
nx, ny, nz = p
b = op.level.blockAt(nx, ny, nz)
if indiscriminate:
if b == 2:
b = 3
if b == doomedBlock:
if checkData:
if op.level.blockDataAt(nx, ny, nz) != doomedBlockData:
continue
saveUndoChunk(nx // 16, nz // 16)
op.level.setBlockAt(nx, ny, nz, op.blockInfo.ID)
op.level.setBlockDataAt(nx, ny, nz, op.blockInfo.blockData)
newcoords.append(p)
return newcoords
def spread(coords):
while len(coords):
start = datetime.now()
num = len(coords)
coords = processCoords(coords)
d = datetime.now() - start
progress = "Did {0} coords in {1}".format(num, d)
log.info(progress)
yield progress
showProgress("Flood fill...", spread([point]), cancel=True)
op.editor.invalidateChunks(dirtyChunks)
op.undoLevel = undoLevel
class Replace(Fill):
name = "Replace"
def createOptions(self, panel, tool):
return Modes.Fill.createOptions(self, panel, tool) + [panel.replaceBlockButton]
def applyToChunkSlices(self, op, chunk, slices, brushBox, brushBoxThisChunk):
blocks = chunk.Blocks[slices]
data = chunk.Data[slices]
brushMask = createBrushMask(op.brushSize, op.brushStyle, brushBox.origin, brushBoxThisChunk, op.noise, op.hollow)
replaceWith = op.options['replaceBlockInfo']
# xxx pasted from fill.py
if op.blockInfo.wildcard:
print "Wildcard replace"
blocksToReplace = []
for i in range(16):
blocksToReplace.append(op.editor.level.materials.blockWithID(op.blockInfo.ID, i))
else:
blocksToReplace = [op.blockInfo]
replaceTable = block_fill.blockReplaceTable(blocksToReplace)
replaceMask = replaceTable[blocks, data]
brushMask &= replaceMask
blocks[brushMask] = replaceWith.ID
data[brushMask] = replaceWith.blockData
class Erode(BrushMode):
name = "Erode"
options = ['erosionStrength']
def createOptions(self, panel, tool):
col = [
panel.modeStyleGrid,
panel.brushSizeRows,
]
col.append(IntInputRow("Strength: ", ref=AttrRef(tool, 'erosionStrength'), min=1, max=20, tooltipText="Number of times to apply erosion. Larger numbers are slower."))
return col
def apply(self, op, point):
brushBox = self.brushBoxForPointAndOptions(point, op.options).expand(1)
if brushBox.volume > 1048576:
raise ValueError("Affected area is too big for this brush mode")
strength = op.options["erosionStrength"]
erosionArea = op.level.extractSchematic(brushBox, entities=False)
if erosionArea is None:
return
blocks = erosionArea.Blocks
bins = numpy.bincount(blocks.ravel())
fillBlockID = bins.argmax()
def getNeighbors(solidBlocks):
neighbors = numpy.zeros(solidBlocks.shape, dtype='uint8')
neighbors[1:-1, 1:-1, 1:-1] += solidBlocks[:-2, 1:-1, 1:-1]
neighbors[1:-1, 1:-1, 1:-1] += solidBlocks[2:, 1:-1, 1:-1]
neighbors[1:-1, 1:-1, 1:-1] += solidBlocks[1:-1, :-2, 1:-1]
neighbors[1:-1, 1:-1, 1:-1] += solidBlocks[1:-1, 2:, 1:-1]
neighbors[1:-1, 1:-1, 1:-1] += solidBlocks[1:-1, 1:-1, :-2]
neighbors[1:-1, 1:-1, 1:-1] += solidBlocks[1:-1, 1:-1, 2:]
return neighbors
for i in range(strength):
solidBlocks = blocks != 0
neighbors = getNeighbors(solidBlocks)
brushMask = createBrushMask(op.brushSize, op.brushStyle)
erodeBlocks = neighbors < 5
erodeBlocks &= (numpy.random.random(erodeBlocks.shape) > 0.3)
erodeBlocks[1:-1, 1:-1, 1:-1] &= brushMask
blocks[erodeBlocks] = 0
solidBlocks = blocks != 0
neighbors = getNeighbors(solidBlocks)
fillBlocks = neighbors > 2
fillBlocks &= ~solidBlocks
fillBlocks[1:-1, 1:-1, 1:-1] &= brushMask
blocks[fillBlocks] = fillBlockID
op.level.copyBlocksFrom(erosionArea, erosionArea.bounds.expand(-1), brushBox.origin + (1, 1, 1))
class Topsoil(BrushMode):
name = "Topsoil"
options = ['naturalEarth', 'topsoilDepth']
def createOptions(self, panel, tool):
depthRow = IntInputRow("Depth: ", ref=AttrRef(tool, 'topsoilDepth'))
naturalRow = CheckBoxLabel("Only Change Natural Earth", ref=AttrRef(tool, 'naturalEarth'))
col = [
panel.modeStyleGrid,
panel.hollowRow,
panel.noiseInput,
panel.brushSizeRows,
panel.blockButton,
depthRow,
naturalRow
]
return col
def applyToChunkSlices(self, op, chunk, slices, brushBox, brushBoxThisChunk):
depth = op.options['topsoilDepth']
blocktype = op.blockInfo
blocks = chunk.Blocks[slices]
data = chunk.Data[slices]
brushMask = createBrushMask(op.brushSize, op.brushStyle, brushBox.origin, brushBoxThisChunk, op.noise, op.hollow)
if op.options['naturalEarth']:
try:
# try to get the block mask from the topsoil filter
import topsoil # @UnresolvedImport
blockmask = topsoil.naturalBlockmask()
blockmask[blocktype.ID] = True
blocktypeMask = blockmask[blocks]
except Exception, e:
print repr(e), " while using blockmask from filters.topsoil"
blocktypeMask = blocks != 0
else:
# topsoil any block
blocktypeMask = blocks != 0
if depth < 0:
blocktypeMask &= (blocks != blocktype.ID)
heightmap = extractHeights(blocktypeMask)
for x, z in itertools.product(*map(xrange, heightmap.shape)):
h = heightmap[x, z]
if h >= brushBoxThisChunk.height:
continue
if depth > 0:
idx = x, z, slice(max(0, h - depth), h)
else:
# negative depth values mean to put a layer above the surface
idx = x, z, slice(h, min(blocks.shape[2], h - depth))
mask = brushMask[idx]
blocks[idx][mask] = blocktype.ID
data[idx][mask] = blocktype.blockData
class Paste(BrushMode):
name = "Paste"
options = ['level'] + ['center' + c for c in 'xyz']
def brushBoxForPointAndOptions(self, point, options={}):
point = [p + options.get('center' + c, 0) for p, c in zip(point, 'xyz')]
return BoundingBox(point, options['level'].size)
def createOptions(self, panel, tool):
col = [panel.brushModeRow]
importButton = Button("Import", action=tool.importPaste)
importLabel = ValueDisplay(width=150, ref=AttrRef(tool, "importFilename"))
importRow = Row((importButton, importLabel))
stack = tool.editor.copyStack
if len(stack) == 0:
tool.importPaste()
else:
tool.loadLevel(stack[0])
tool.centery = 0
tool.centerx = -(tool.level.Width / 2)
tool.centerz = -(tool.level.Length / 2)
cx, cy, cz = [IntInputRow(c, ref=AttrRef(tool, "center" + c), max=a, min=-a)
for a, c in zip(tool.level.size, "xyz")]
centerRow = Row((cx, cy, cz))
col.extend([importRow, centerRow])
return col
def apply(self, op, point):
level = op.options['level']
point = [p + op.options['center' + c] for p, c in zip(point, 'xyz')]
return op.level.copyBlocksFromIter(level, level.bounds, point, create=True)
class BrushOperation(Operation):
def __init__(self, editor, level, points, options):
super(BrushOperation, self).__init__(editor, level)
# if options is None: options = {}
self.options = options
self.editor = editor
if isinstance(points[0], (int, float)):
points = [points]
self.points = points
self.brushSize = options['brushSize']
self.blockInfo = options['blockInfo']
self.brushStyle = options['brushStyle']
self.brushMode = options['brushMode']
if max(self.brushSize) > BrushTool.maxBrushSize:
self.brushSize = (BrushTool.maxBrushSize,) * 3
if max(self.brushSize) < 1:
self.brushSize = (1, 1, 1)
boxes = [self.brushMode.brushBoxForPointAndOptions(p, options) for p in points]
self._dirtyBox = reduce(lambda a, b: a.union(b), boxes)
brushStyles = ["Round", "Square", "Diamond"]
# brushModeNames = ["Fill", "Flood Fill", "Replace", "Erode", "Topsoil", "Paste"] # "Smooth", "Flatten", "Raise", "Lower", "Build", "Erode", "Evert"]
brushModeClasses = [
Modes.Fill,
Modes.FloodFill,
Modes.Replace,
Modes.Erode,
Modes.Topsoil,
Modes.Paste
]
@property
def noise(self):
return self.options.get('brushNoise', 100)
@property
def hollow(self):
return self.options.get('brushHollow', False)
def dirtyBox(self):
return self._dirtyBox
def perform(self, recordUndo=True):
if recordUndo:
self.undoLevel = self.extractUndo(self.level, self._dirtyBox)
def _perform():
yield 0, len(self.points), "Applying {0} brush...".format(self.brushMode.name)
if self.brushMode.apply is not NotImplemented: #xxx double negative
for i, point in enumerate(self.points):
f = self.brushMode.apply(self, point)
if hasattr(f, "__iter__"):
for progress in f:
yield progress
else:
yield i, len(self.points), "Applying {0} brush...".format(self.brushMode.name)
else:
for j, cPos in enumerate(self._dirtyBox.chunkPositions):
if not self.level.containsChunk(*cPos):
continue
chunk = self.level.getChunk(*cPos)
for i, point in enumerate(self.points):
f = self.brushMode.applyToChunk(self, chunk, point)
if hasattr(f, "__iter__"):
for progress in f:
yield progress
else:
yield j * len(self.points) + i, len(self.points) * self._dirtyBox.chunkCount, "Applying {0} brush...".format(self.brushMode.name)
chunk.chunkChanged()
if len(self.points) > 10:
showProgress("Performing brush...", _perform(), cancel=True)
else:
exhaust(_perform())
class BrushPanel(Panel):
def __init__(self, tool):
Panel.__init__(self)
self.tool = tool
self.brushModeButton = ChoiceButton([m.name for m in tool.brushModes],
width=150,
choose=self.brushModeChanged)
self.brushModeButton.selectedChoice = tool.brushMode.name
self.brushModeRow = Row((Label("Mode:"), self.brushModeButton))
self.brushStyleButton = ValueButton(width=self.brushModeButton.width,
ref=AttrRef(tool, "brushStyle"),
action=tool.swapBrushStyles)
self.brushStyleButton.tooltipText = "Shortcut: ALT-1"
self.brushStyleRow = Row((Label("Brush:"), self.brushStyleButton))
self.modeStyleGrid = Column([
self.brushModeRow,
self.brushStyleRow,
])
shapeRows = []
for d in ["L", "W", "H"]:
l = Label(d)
f = IntField(ref=getattr(BrushSettings, "brushSize" + d).propertyRef(), min=1, max=tool.maxBrushSize)
row = Row((l, f))
shapeRows.append(row)
self.brushSizeRows = Column(shapeRows)
self.noiseInput = IntInputRow("Chance: ", ref=AttrRef(tool, "brushNoise"), min=0, max=100)
hollowCheckBox = CheckBox(ref=AttrRef(tool, "brushHollow"))
hollowLabel = Label("Hollow")
hollowLabel.mouse_down = hollowCheckBox.mouse_down
hollowLabel.tooltipText = hollowCheckBox.tooltipText = "Shortcut: ALT-3"
self.hollowRow = Row((hollowCheckBox, hollowLabel))
self.blockButton = blockButton = BlockButton(
tool.editor.level.materials,
ref=AttrRef(tool, 'blockInfo'),
recentBlocks=tool.recentFillBlocks,
allowWildcards=(tool.brushMode.name == "Replace"))
# col = [modeStyleGrid, hollowRow, noiseInput, shapeRows, blockButton]
self.replaceBlockButton = replaceBlockButton = BlockButton(
tool.editor.level.materials,
ref=AttrRef(tool, 'replaceBlockInfo'),
recentBlocks=tool.recentReplaceBlocks)
col = tool.brushMode.createOptions(self, tool)
if self.tool.brushMode.name != "Flood Fill":
spaceRow = IntInputRow("Line Spacing", ref=AttrRef(tool, "minimumSpacing"), min=1, tooltipText="Hold SHIFT to draw lines")
col.append(spaceRow)
col = Column(col)
self.add(col)
self.shrink_wrap()
def brushModeChanged(self):
self.tool.brushMode = self.brushModeButton.selectedChoice
def pickFillBlock(self):
self.blockButton.action()
self.tool.blockInfo = self.blockButton.blockInfo
self.tool.setupPreview()
def pickReplaceBlock(self):
self.replaceBlockButton.action()
self.tool.replaceBlockInfo = self.replaceBlockButton.blockInfo
self.tool.setupPreview()
def swap(self):
t = self.blockButton.recentBlocks
self.blockButton.recentBlocks = self.replaceBlockButton.recentBlocks
self.replaceBlockButton.recentBlocks = t
self.blockButton.updateRecentBlockView()
self.replaceBlockButton.updateRecentBlockView()
b = self.blockButton.blockInfo
self.blockButton.blockInfo = self.replaceBlockButton.blockInfo
self.replaceBlockButton.blockInfo = b
class BrushToolOptions(ToolOptions):
def __init__(self, tool):
Panel.__init__(self)
alphaField = FloatField(ref=AttrRef(tool, 'brushAlpha'), min=0.0, max=1.0, width=60)
alphaField.increment = 0.1
alphaRow = Row((Label("Alpha: "), alphaField))
autoChooseCheckBox = CheckBoxLabel("Choose Block Immediately",
ref=AttrRef(tool, "chooseBlockImmediately"),
tooltipText="When the brush tool is chosen, prompt for a block type.")
updateOffsetCheckBox = CheckBoxLabel("Reset Distance When Brush Size Changes",
ref=AttrRef(tool, "updateBrushOffset"),
tooltipText="Whenever the brush size changes, reset the distance to the brush blocks.")
col = Column((Label("Brush Options"), alphaRow, autoChooseCheckBox, updateOffsetCheckBox, Button("OK", action=self.dismiss)))
self.add(col)
self.shrink_wrap()
return
from clone import CloneTool
class BrushTool(CloneTool):
tooltipText = "Brush\nRight-click for options"
toolIconName = "brush"
minimumSpacing = 1
def __init__(self, *args):
CloneTool.__init__(self, *args)
self.optionsPanel = BrushToolOptions(self)
self.recentFillBlocks = []
self.recentReplaceBlocks = []
self.draggedPositions = []
self.brushModes = [c() for c in BrushOperation.brushModeClasses]
for m in self.brushModes:
self.options.extend(m.options)
self._brushMode = self.brushModes[0]
BrushSettings.updateBrushOffset.addObserver(self)
BrushSettings.brushSizeW.addObserver(self, 'brushSizeW', callback=self._setBrushSize)
BrushSettings.brushSizeH.addObserver(self, 'brushSizeH', callback=self._setBrushSize)
BrushSettings.brushSizeL.addObserver(self, 'brushSizeL', callback=self._setBrushSize)
panel = None
def _setBrushSize(self, _):
if self.updateBrushOffset:
self.reticleOffset = self.offsetMax()
self.resetToolDistance()
self.previewDirty = True
previewDirty = False
updateBrushOffset = True
_reticleOffset = 1
naturalEarth = True
erosionStrength = 1
indiscriminate = False
@property
def reticleOffset(self):
if self.brushMode.name == "Flood Fill":
return 0
return self._reticleOffset
@reticleOffset.setter
def reticleOffset(self, val):
self._reticleOffset = val
brushSizeW, brushSizeH, brushSizeL = 1, 1, 1
@property
def brushSize(self):
if self.brushMode.name == "Flood Fill":
return 1, 1, 1
return [self.brushSizeW, self.brushSizeH, self.brushSizeL]
@brushSize.setter
def brushSize(self, val):
(w, h, l) = [max(1, min(i, self.maxBrushSize)) for i in val]
BrushSettings.brushSizeH.set(h)
BrushSettings.brushSizeL.set(l)
BrushSettings.brushSizeW.set(w)
maxBrushSize = 4096
brushStyles = BrushOperation.brushStyles
brushStyle = brushStyles[0]
brushModes = None
@property
def brushMode(self):
return self._brushMode
@brushMode.setter
def brushMode(self, val):
if isinstance(val, str):
val = [b for b in self.brushModes if b.name == val][0]
self._brushMode = val
self.hidePanel()
self.showPanel()
brushNoise = 100
brushHollow = False
topsoilDepth = 1
chooseBlockImmediately = BrushSettings.chooseBlockImmediately.configProperty()
_blockInfo = pymclevel.alphaMaterials.Stone
@property
def blockInfo(self):
return self._blockInfo
@blockInfo.setter
def blockInfo(self, bi):
self._blockInfo = bi
self.setupPreview()
_replaceBlockInfo = pymclevel.alphaMaterials.Stone
@property
def replaceBlockInfo(self):
return self._replaceBlockInfo
@replaceBlockInfo.setter
def replaceBlockInfo(self, bi):
self._replaceBlockInfo = bi
self.setupPreview()
@property
def brushAlpha(self):
return BrushSettings.alpha.get()
@brushAlpha.setter
def brushAlpha(self, f):
f = min(1.0, max(0.0, f))
BrushSettings.alpha.set(f)
self.setupPreview()
def importPaste(self):
clipFilename = mcplatform.askOpenFile(title='Choose a schematic or level...', schematics=True)
# xxx mouthful
if clipFilename:
try:
self.loadLevel(pymclevel.fromFile(clipFilename, readonly=True))
except Exception, e:
alert("Failed to load file %s" % clipFilename)
self.brushMode = "Fill"
return
def loadLevel(self, level):
self.level = level
self.minimumSpacing = min([s / 4 for s in level.size])
self.centerx, self.centery, self.centerz = -level.Width / 2, 0, -level.Length / 2
CloneTool.setupPreview(self)
@property
def importFilename(self):
if self.level:
return basename(self.level.filename or "No name")
return "Nothing selected"
@property
def statusText(self):
return "Click and drag to place blocks. ALT-Click to use the block under the cursor. {R} to increase and {F} to decrease size. {E} to rotate, {G} to roll. Mousewheel to adjust distance.".format(
R=config.config.get("Keys", "Roll").upper(),
F=config.config.get("Keys", "Flip").upper(),
E=config.config.get("Keys", "Rotate").upper(),
G=config.config.get("Keys", "Mirror").upper(),
)
@property
def worldTooltipText(self):
if pygame.key.get_mods() & pygame.KMOD_ALT:
try:
if self.editor.blockFaceUnderCursor is None:
return
pos = self.editor.blockFaceUnderCursor[0]
blockID = self.editor.level.blockAt(*pos)
blockdata = self.editor.level.blockDataAt(*pos)
return "Click to use {0} ({1}:{2})".format(self.editor.level.materials.names[blockID][blockdata], blockID, blockdata)
except Exception, e:
return repr(e)
if self.brushMode.name == "Flood Fill":
try:
if self.editor.blockFaceUnderCursor is None:
return
pos = self.editor.blockFaceUnderCursor[0]
blockID = self.editor.level.blockAt(*pos)
blockdata = self.editor.level.blockDataAt(*pos)
return "Click to replace {0} ({1}:{2})".format(self.editor.level.materials.names[blockID][blockdata], blockID, blockdata)
except Exception, e:
return repr(e)
def swapBrushStyles(self):
brushStyleIndex = self.brushStyles.index(self.brushStyle) + 1
brushStyleIndex %= len(self.brushStyles)
self.brushStyle = self.brushStyles[brushStyleIndex]
self.setupPreview()
def swapBrushModes(self):
brushModeIndex = self.brushModes.index(self.brushMode) + 1
brushModeIndex %= len(self.brushModes)
self.brushMode = self.brushModes[brushModeIndex]
options = [
'blockInfo',
'brushStyle',
'brushMode',
'brushSize',
'brushNoise',
'brushHollow',
'replaceBlockInfo',
]
def getBrushOptions(self):
return dict(((key, getattr(self, key))
for key
in self.options))
draggedDirection = (0, 0, 0)
centerx = centery = centerz = 0
@alertException
def mouseDown(self, evt, pos, direction):
if pygame.key.get_mods() & pygame.KMOD_ALT:
id = self.editor.level.blockAt(*pos)
data = self.editor.level.blockDataAt(*pos)
if self.brushMode.name == "Replace":
self.panel.replaceBlockButton.blockInfo = self.editor.level.materials.blockWithID(id, data)
else:
self.panel.blockButton.blockInfo = self.editor.level.materials.blockWithID(id, data)
return
self.draggedDirection = direction
point = [p + d * self.reticleOffset for p, d in zip(pos, direction)]
self.dragLineToPoint(point)
@alertException
def mouseDrag(self, evt, pos, _dir):
direction = self.draggedDirection
if self.brushMode.name != "Flood Fill":
if len(self.draggedPositions): # if self.isDragging
self.lastPosition = lastPoint = self.draggedPositions[-1]
point = [p + d * self.reticleOffset for p, d in zip(pos, direction)]
if any([abs(a - b) >= self.minimumSpacing
for a, b in zip(point, lastPoint)]):
self.dragLineToPoint(point)
def dragLineToPoint(self, point):
if self.brushMode.name == "Flood Fill":
self.draggedPositions = [point]
return
if pygame.key.get_mods() & pygame.KMOD_SHIFT:
if len(self.draggedPositions):
points = bresenham.bresenham(self.draggedPositions[-1], point)
self.draggedPositions.extend(points[::self.minimumSpacing][1:])
elif self.lastPosition is not None:
points = bresenham.bresenham(self.lastPosition, point)
self.draggedPositions.extend(points[::self.minimumSpacing][1:])
else:
self.draggedPositions.append(point)
@alertException
def mouseUp(self, evt, pos, direction):
if 0 == len(self.draggedPositions):
return
size = self.brushSize
# point = self.getReticlePoint(pos, direction)
if self.brushMode.name == "Flood Fill":
self.draggedPositions = self.draggedPositions[-1:]
op = BrushOperation(self.editor,
self.editor.level,
self.draggedPositions,
self.getBrushOptions())
box = op.dirtyBox()
self.editor.addOperation(op)
self.editor.addUnsavedEdit()
self.editor.invalidateBox(box)
self.lastPosition = self.draggedPositions[-1]
self.draggedPositions = []
def toolEnabled(self):
return True
def rotate(self):
offs = self.reticleOffset
dist = self.editor.cameraToolDistance
W, H, L = self.brushSize
self.brushSize = L, H, W
self.reticleOffset = offs
self.editor.cameraToolDistance = dist
def mirror(self):
offs = self.reticleOffset
dist = self.editor.cameraToolDistance
W, H, L = self.brushSize
self.brushSize = W, L, H
self.reticleOffset = offs
self.editor.cameraToolDistance = dist
def toolReselected(self):
if self.brushMode.name == "Replace":
self.panel.pickReplaceBlock()
else:
self.panel.pickFillBlock()
def flip(self):
self.decreaseBrushSize()
def roll(self):
self.increaseBrushSize()
def swap(self):
self.panel.swap()
def decreaseBrushSize(self):
self.brushSize = [i - 1 for i in self.brushSize]
# self.setupPreview()
def increaseBrushSize(self):
self.brushSize = [i + 1 for i in self.brushSize]
@alertException
def setupPreview(self):
self.previewDirty = False
brushSize = self.brushSize
brushStyle = self.brushStyle
if self.brushMode.name == "Replace":
blockInfo = self.replaceBlockInfo
else:
blockInfo = self.blockInfo
class FakeLevel(pymclevel.MCLevel):
filename = "Fake Level"
materials = self.editor.level.materials
def __init__(self):
self.chunkCache = {}
Width, Height, Length = brushSize
zerolight = numpy.zeros((16, 16, Height), dtype='uint8')
zerolight[:] = 15
def getChunk(self, cx, cz):
if (cx, cz) in self.chunkCache:
return self.chunkCache[cx, cz]
class FakeBrushChunk(pymclevel.level.FakeChunk):
Entities = []
TileEntities = []
f = FakeBrushChunk()
f.world = self
f.chunkPosition = (cx, cz)
mask = createBrushMask(brushSize, brushStyle, (0, 0, 0), BoundingBox((cx << 4, 0, cz << 4), (16, self.Height, 16)))
f.Blocks = numpy.zeros(mask.shape, dtype='uint8')
f.Data = numpy.zeros(mask.shape, dtype='uint8')
f.BlockLight = self.zerolight
f.SkyLight = self.zerolight
if blockInfo.ID:
f.Blocks[mask] = blockInfo.ID
f.Data[mask] = blockInfo.blockData
else:
f.Blocks[mask] = 255
self.chunkCache[cx, cz] = f
return f
self.level = FakeLevel()
CloneTool.setupPreview(self, alpha=self.brushAlpha)
def resetToolDistance(self):
distance = max(self.editor.cameraToolDistance, 6 + max(self.brushSize) * 1.25)
# print "Adjusted distance", distance, max(self.brushSize) * 1.25
self.editor.cameraToolDistance = distance
def toolSelected(self):
if self.chooseBlockImmediately:
blockPicker = BlockPicker(
self.blockInfo,
self.editor.level.materials,
allowWildcards=self.brushMode.name == "Replace")
if blockPicker.present():
self.blockInfo = blockPicker.blockInfo
if self.updateBrushOffset:
self.reticleOffset = self.offsetMax()
self.resetToolDistance()
self.setupPreview()
self.showPanel()
# def cancel(self):
# self.hidePanel()
# super(BrushTool, self).cancel()
def showPanel(self):
if self.panel:
self.panel.parent.remove(self.panel)
panel = BrushPanel(self)
panel.centery = self.editor.centery
panel.left = self.editor.left
panel.anchor = "lwh"
self.panel = panel
self.editor.add(panel)
def increaseToolReach(self):
# self.reticleOffset = max(self.reticleOffset-1, 0)
if self.editor.mainViewport.mouseMovesCamera and not self.editor.longDistanceMode:
return False
self.reticleOffset = self.reticleOffset + 1
return True
def decreaseToolReach(self):
if self.editor.mainViewport.mouseMovesCamera and not self.editor.longDistanceMode:
return False
self.reticleOffset = max(self.reticleOffset - 1, 0)
return True
def resetToolReach(self):
if self.editor.mainViewport.mouseMovesCamera and not self.editor.longDistanceMode:
self.resetToolDistance()
else:
self.reticleOffset = self.offsetMax()
return True
cameraDistance = EditorTool.cameraDistance
def offsetMax(self):
return max(1, ((0.5 * max(self.brushSize)) + 1))
def getReticleOffset(self):
return self.reticleOffset
def getReticlePoint(self, pos, direction):
if len(self.draggedPositions):
direction = self.draggedDirection
return map(lambda a, b: a + (b * self.getReticleOffset()), pos, direction)
def drawToolReticle(self):
for pos in self.draggedPositions:
drawTerrainCuttingWire(BoundingBox(pos, (1, 1, 1)),
(0.75, 0.75, 0.1, 0.4),
(1.0, 1.0, 0.5, 1.0))
lastPosition = None
def drawTerrainReticle(self):
if pygame.key.get_mods() & pygame.KMOD_ALT:
# eyedropper mode
self.editor.drawWireCubeReticle(color=(0.2, 0.6, 0.9, 1.0))
else:
pos, direction = self.editor.blockFaceUnderCursor
reticlePoint = self.getReticlePoint(pos, direction)
self.editor.drawWireCubeReticle(position=reticlePoint)
if reticlePoint != pos:
GL.glColor4f(1.0, 1.0, 0.0, 0.7)
with gl.glBegin(GL.GL_LINES):
GL.glVertex3f(*map(lambda a: a + 0.5, reticlePoint)) # center of reticle block
GL.glVertex3f(*map(lambda a, b: a + 0.5 + b * 0.5, pos, direction)) # top side of surface block
if self.previewDirty:
self.setupPreview()
dirtyBox = self.brushMode.brushBoxForPointAndOptions(reticlePoint, self.getBrushOptions())
self.drawTerrainPreview(dirtyBox.origin)
if pygame.key.get_mods() & pygame.KMOD_SHIFT and self.lastPosition and self.brushMode.name != "Flood Fill":
GL.glColor4f(1.0, 1.0, 1.0, 0.7)
with gl.glBegin(GL.GL_LINES):
GL.glVertex3f(*map(lambda a: a + 0.5, self.lastPosition))
GL.glVertex3f(*map(lambda a: a + 0.5, reticlePoint))
def updateOffsets(self):
pass
def selectionChanged(self):
pass
def option1(self):
self.swapBrushStyles()
def option2(self):
self.swapBrushModes()
def option3(self):
self.brushHollow = not self.brushHollow
def createBrushMask(shape, style="Round", offset=(0, 0, 0), box=None, chance=100, hollow=False):
"""
Return a boolean array for a brush with the given shape and style.
If 'offset' and 'box' are given, then the brush is offset into the world
and only the part of the world contained in box is returned as an array
"""
# we are returning indices for a Blocks array, so swap axes
if box is None:
box = BoundingBox(offset, shape)
if chance < 100 or hollow:
box = box.expand(1)
outputShape = box.size
outputShape = (outputShape[0], outputShape[2], outputShape[1])
shape = shape[0], shape[2], shape[1]
offset = numpy.array(offset) - numpy.array(box.origin)
offset = offset[[0, 2, 1]]
inds = numpy.indices(outputShape, dtype=float)
halfshape = numpy.array([(i >> 1) - ((i & 1 == 0) and 0.5 or 0) for i in shape])
blockCenters = inds - halfshape[:, newaxis, newaxis, newaxis]
blockCenters -= offset[:, newaxis, newaxis, newaxis]
# odd diameter means measure from the center of the block at 0,0,0 to each block center
# even diameter means measure from the 0,0,0 grid point to each block center
# if diameter & 1 == 0: blockCenters += 0.5
shape = numpy.array(shape, dtype='float32')
# if not isSphere(shape):
if style == "Round":
blockCenters *= blockCenters
shape /= 2
shape *= shape
blockCenters /= shape[:, newaxis, newaxis, newaxis]
distances = sum(blockCenters, 0)
mask = distances < 1
elif style == "Square":
# mask = ones(outputShape, dtype=bool)
# mask = blockCenters[:, newaxis, newaxis, newaxis] < shape
blockCenters /= shape[:, newaxis, newaxis, newaxis]
distances = numpy.absolute(blockCenters).max(0)
mask = distances < .5
elif style == "Diamond":
blockCenters = numpy.abs(blockCenters)
shape /= 2
blockCenters /= shape[:, newaxis, newaxis, newaxis]
distances = sum(blockCenters, 0)
mask = distances < 1
else:
raise ValueError, "Unknown style: " + style
if (chance < 100 or hollow) and max(shape) > 1:
threshold = chance / 100.0
exposedBlockMask = numpy.ones(shape=outputShape, dtype='bool')
exposedBlockMask[:] = mask
submask = mask[1:-1, 1:-1, 1:-1]
exposedBlockSubMask = exposedBlockMask[1:-1, 1:-1, 1:-1]
exposedBlockSubMask[:] = False
for dim in (0, 1, 2):
slices = [slice(1, -1), slice(1, -1), slice(1, -1)]
slices[dim] = slice(None, -2)
exposedBlockSubMask |= (submask & (mask[slices] != submask))
slices[dim] = slice(2, None)
exposedBlockSubMask |= (submask & (mask[slices] != submask))
if hollow:
mask[~exposedBlockMask] = False
if chance < 100:
rmask = numpy.random.random(mask.shape) < threshold
mask[exposedBlockMask] = rmask[exposedBlockMask]
if chance < 100 or hollow:
return mask[1:-1, 1:-1, 1:-1]
else:
return mask