
"Record Undo" is reset when loading or saving a level. Long undo operations can be canceled.
1164 lines
41 KiB
Python
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
|