
"Record Undo" is reset when loading or saving a level. Long undo operations can be canceled.
118 lines
4.0 KiB
Python
118 lines
4.0 KiB
Python
import atexit
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
import albow
|
|
from pymclevel import BoundingBox
|
|
import numpy
|
|
from albow.root import Cancel
|
|
import pymclevel
|
|
from mceutils import showProgress
|
|
from pymclevel.mclevelbase import exhaust
|
|
|
|
undo_folder = os.path.join(tempfile.gettempdir(), "mcedit_undo", str(os.getpid()))
|
|
|
|
def mkundotemp():
|
|
if not os.path.exists(undo_folder):
|
|
os.makedirs(undo_folder)
|
|
|
|
return tempfile.mkdtemp("mceditundo", dir=undo_folder)
|
|
|
|
atexit.register(shutil.rmtree, undo_folder, True)
|
|
|
|
class Operation(object):
|
|
changedLevel = True
|
|
undoLevel = None
|
|
|
|
def __init__(self, editor, level):
|
|
self.editor = editor
|
|
self.level = level
|
|
|
|
def extractUndo(self, level, box):
|
|
if isinstance(level, pymclevel.MCInfdevOldLevel):
|
|
return self.extractUndoChunks(level, box.chunkPositions, box.chunkCount)
|
|
else:
|
|
return self.extractUndoSchematic(level, box)
|
|
|
|
def extractUndoChunks(self, level, chunks, chunkCount = None):
|
|
if not isinstance(level, pymclevel.MCInfdevOldLevel):
|
|
chunks = numpy.array(list(chunks))
|
|
mincx, mincz = numpy.min(chunks, 0)
|
|
maxcx, maxcz = numpy.max(chunks, 0)
|
|
box = BoundingBox((mincx << 4, 0, mincz << 4), (maxcx << 4, level.Height, maxcz << 4))
|
|
|
|
return self.extractUndoSchematic(level, box)
|
|
|
|
undoLevel = pymclevel.MCInfdevOldLevel(mkundotemp(), create=True)
|
|
if not chunkCount:
|
|
try:
|
|
chunkCount = len(chunks)
|
|
except TypeError:
|
|
chunkCount = -1
|
|
|
|
def _extractUndo():
|
|
yield 0, 0, "Recording undo..."
|
|
for i, (cx, cz) in enumerate(chunks):
|
|
undoLevel.copyChunkFrom(level, cx, cz)
|
|
yield i, chunkCount, "Copying chunk %s..." % ((cx, cz),)
|
|
undoLevel.saveInPlace()
|
|
|
|
if chunkCount > 25 or chunkCount < 1:
|
|
if "Canceled" == showProgress("Recording undo...", _extractUndo(), cancel=True):
|
|
if albow.ask("Continue with undo disabled?", ["Continue", "Cancel"]) == "Cancel":
|
|
raise Cancel
|
|
else:
|
|
return None
|
|
else:
|
|
exhaust(_extractUndo())
|
|
|
|
return undoLevel
|
|
|
|
def extractUndoSchematic(self, level, box):
|
|
if box.volume > 131072:
|
|
sch = showProgress("Recording undo...", level.extractZipSchematicIter(box), cancel=True)
|
|
else:
|
|
sch = level.extractZipSchematic(box)
|
|
if sch == "Cancel":
|
|
raise Cancel
|
|
if sch:
|
|
sch.sourcePoint = box.origin
|
|
|
|
return sch
|
|
|
|
|
|
# represents a single undoable operation
|
|
def perform(self, recordUndo=True):
|
|
" Perform the operation. Record undo information if recordUndo"
|
|
|
|
def undo(self):
|
|
""" Undo the operation. Ought to leave the Operation in a state where it can be performed again.
|
|
Default implementation copies all chunks in undoLevel back into level. Non-chunk-based operations
|
|
should override this."""
|
|
|
|
if self.undoLevel:
|
|
|
|
def _undo():
|
|
yield 0, 0, "Undoing..."
|
|
if hasattr(self.level, 'copyChunkFrom'):
|
|
for i, (cx, cz) in enumerate(self.undoLevel.allChunks):
|
|
self.level.copyChunkFrom(self.undoLevel, cx, cz)
|
|
yield i, self.undoLevel.chunkCount, "Copying chunk %s..." % ((cx, cz),)
|
|
else:
|
|
for i in self.level.copyBlocksFromIter(self.undoLevel, self.undoLevel.bounds, self.undoLevel.sourcePoint, biomes=True):
|
|
yield i, self.undoLevel.chunkCount, "Copying..."
|
|
|
|
if self.undoLevel.chunkCount > 25:
|
|
showProgress("Undoing...", _undo())
|
|
else:
|
|
exhaust(_undo())
|
|
|
|
self.editor.invalidateChunks(self.undoLevel.allChunks)
|
|
|
|
|
|
def dirtyBox(self):
|
|
""" The region modified by the operation.
|
|
Return None to indicate no blocks were changed.
|
|
"""
|
|
return None
|