asciiflow2/js-lib/state.js

332 lines
9.3 KiB
JavaScript

/**
* Holds the entire state of the diagram as a 2D array of cells
* and provides methods to modify the current state.
*
* @constructor
*/
ascii.State = function() {
/** @type {Array.<Array.<ascii.Cell>>} */
this.cells = new Array(MAX_GRID_WIDTH);
/** @type {Array.<ascii.MappedCell>} */
this.scratchCells = new Array();
/** @type {boolean} */
this.dirty = true;
/** @type {Array.<Array.<ascii.MappedValue>>} */
this.undoStates = new Array();
/** @type {Array.<Array.<ascii.MappedValue>>} */
this.redoStates = new Array();
for (var i = 0; i < this.cells.length; i++) {
this.cells[i] = new Array(MAX_GRID_HEIGHT);
for (var j = 0; j < this.cells[i].length; j++) {
this.cells[i][j] = new ascii.Cell();
}
}
};
/**
* This clears the entire state, but is undoable.
*/
ascii.State.prototype.clear = function() {
for (var i = 0; i < this.cells.length; i++) {
for (var j = 0; j < this.cells[i].length; j++) {
var position = new ascii.Vector(i, j);
if (this.cells[i][j].getRawValue() != null) {
this.drawValue(new ascii.Vector(i, j), ERASE_CHAR);
}
}
}
this.commitDraw();
};
/**
* Returns the cell at the given coordinates.
*
* @param {ascii.Vector} vector
* @return {ascii.Cell}
*/
ascii.State.prototype.getCell = function(vector) {
return this.cells[vector.x][vector.y];
};
/**
* Sets the cells scratch (uncommitted) value at the given position.
*
* @param {ascii.Vector} position
* @param {?string} value
*/
ascii.State.prototype.drawValue = function(position, value) {
var cell = this.getCell(position);
this.scratchCells.push(new ascii.MappedCell(position, cell));
cell.scratchValue = value;
this.dirty = true;
};
/**
* Sets the cells scratch (uncommitted) value at the given position
* iff the value is different to what it already is.
*
* @param {ascii.Vector} position
* @param {?string} value
*/
ascii.State.prototype.drawValueIncremental = function(position, value) {
if (this.getCell(position).getRawValue() != value) {
this.drawValue(position, value);
}
};
/**
* Clears the current drawing scratchpad.
*/
ascii.State.prototype.clearDraw = function() {
for (var i in this.scratchCells) {
this.scratchCells[i].cell.scratchValue = null;
}
this.scratchCells.length = 0;
};
/**
* Returns the draw value of a cell at the given position.
*
* @param {ascii.Vector} position
* @return {?string}
*/
ascii.State.prototype.getDrawValue = function(position) {
var cell = this.getCell(position);
var value = cell.scratchValue != null ? cell.scratchValue : cell.value;
var isSpecial = SPECIAL_VALUES.indexOf(value) != -1;
var isAltSpecial = ALT_SPECIAL_VALUES.indexOf(value) != -1;
if (!isSpecial && !isAltSpecial) {
return value;
}
// Magic time.
var context = this.getContext(position);
if (context.left && context.right && !context.up && !context.down) {
return SPECIAL_LINE_H;
}
if (!context.left && !context.right && context.up && context.down) {
return SPECIAL_LINE_V;
}
if (context.sum() == 4) {
return SPECIAL_LINE_H;
}
if (isAltSpecial && context.sum() == 3) {
if (!context.left) {
return SPECIAL_ARROW_LEFT;
}
if (!context.up) {
return SPECIAL_ARROW_UP;
}
if (!context.down) {
return SPECIAL_ARROW_DOWN;
}
if (!context.right) {
return SPECIAL_ARROW_RIGHT;
}
}
if ((isSpecial || isAltSpecial) && context.sum() == 3) {
this.extendContext(position, context);
if (!context.right && context.leftup && context.leftdown) {
return SPECIAL_LINE_V;
}
if (!context.left && context.rightup && context.rightdown) {
return SPECIAL_LINE_V;
}
if (!context.down && context.leftup && context.rightup) {
return SPECIAL_LINE_H;
}
if (!context.up && context.rightdown && context.leftdown) {
return SPECIAL_LINE_H;
}
return SPECIAL_VALUE;
}
if (isAltSpecial && context.sum() == 1) {
if (context.left) {
return SPECIAL_ARROW_RIGHT;
}
if (context.up) {
return SPECIAL_ARROW_DOWN;
}
if (context.down) {
return SPECIAL_ARROW_UP;
}
if (context.right) {
return SPECIAL_ARROW_LEFT;
}
}
return value;
};
/**
* @param {ascii.Vector} position
* @return {ascii.CellContext}
*/
ascii.State.prototype.getContext = function(position) {
var left = this.getCell(position.add(DIR_LEFT)).isSpecial();
var right = this.getCell(position.add(DIR_RIGHT)).isSpecial();
var up = this.getCell(position.add(DIR_UP)).isSpecial();
var down = this.getCell(position.add(DIR_DOWN)).isSpecial();
return new ascii.CellContext(left, right, up, down);
};
/**
* @param {ascii.Vector} position
* @param {ascii.CellContext} context
*/
ascii.State.prototype.extendContext = function(position, context) {
context.leftup = this.getCell(position.add(DIR_LEFT).add(DIR_UP)).isSpecial();
context.rightup = this.getCell(position.add(DIR_RIGHT).add(DIR_UP)).isSpecial();
context.leftdown = this.getCell(position.add(DIR_LEFT).add(DIR_DOWN)).isSpecial();
context.rightdown = this.getCell(position.add(DIR_RIGHT).add(DIR_DOWN)).isSpecial();
};
/**
* Ends the current draw, commiting anything currently drawn the scratchpad.
* @param {boolean=} opt_undo
*/
ascii.State.prototype.commitDraw = function(opt_undo) {
var oldValues = [];
// Dedupe the scratch values, or this causes havoc for history management.
var positions = this.scratchCells.map(function(value) {
return value.position.x.toString() + value.position.y.toString();
});
var scratchCellsUnique =
this.scratchCells.filter(function(value, index, arr) {
return positions.indexOf(positions[index]) == index;
});
this.scratchCells.length = 0;
for (var i in scratchCellsUnique) {
var position = scratchCellsUnique[i].position;
var cell = scratchCellsUnique[i].cell;
// Push the effective old value unto the array.
oldValues.push(new ascii.MappedValue(position,
cell.value != null ? cell.value : ' '));
var newValue = cell.getRawValue();
if (newValue == ERASE_CHAR || newValue == ' ') {
newValue = null;
}
// Let's store the actual drawed value, so behaviour matches what the user sees.
if (cell.isSpecial()) {
newValue = this.getDrawValue(position);
}
cell.scratchValue = null;
cell.value = newValue;
}
var stateStack = opt_undo ? this.redoStates : this.undoStates;
if (oldValues.length > 0) {
// If we have too many states, clear one out.
if (stateStack.length > MAX_UNDO) {
stateStack.shift();
}
stateStack.push(oldValues);
}
this.dirty = true;
};
/**
* Undoes the last committed state.
*/
ascii.State.prototype.undo = function() {
if (this.undoStates.length == 0) { return; }
var lastState = this.undoStates.pop();
for (var i in lastState) {
var mappedValue = lastState[i];
this.drawValue(mappedValue.position, mappedValue.value);
}
this.commitDraw(true);
};
/**
* Redoes the last undone.
*/
ascii.State.prototype.redo = function() {
if (this.redoStates.length == 0) { return; }
var lastState = this.redoStates.pop();
for (var i in lastState) {
var mappedValue = lastState[i];
this.drawValue(mappedValue.position, mappedValue.value);
}
this.commitDraw();
};
/**
* Outputs the entire contents of the diagram as text.
* @param {ascii.Box=} opt_box
* @return {string}
*/
ascii.State.prototype.outputText = function(opt_box) {
// Find the first/last cells in the diagram so we don't output everything.
var start = new ascii.Vector(Number.MAX_VALUE, Number.MAX_VALUE);
var end = new ascii.Vector(-1, -1);
if (!opt_box) {
for (var i = 0; i < this.cells.length; i++) {
for (var j = 0; j < this.cells[i].length; j++) {
if (this.cells[i][j].getRawValue() != null) {
if (i < start.x) { start.x = i; }
if (j < start.y) { start.y = j; }
if (i > end.x) { end.x = i; }
if (j > end.y) { end.y = j; }
}
}
}
if (end.x < 0) { return '' }
} else {
start = opt_box.topLeft();
end = opt_box.bottomRight();
}
var output = '';
for (var j = start.y; j <= end.y; j++) {
var line = '';
for (var i = start.x; i <= end.x; i++) {
var val = this.getDrawValue(new ascii.Vector(i, j));
line += (val == null ? ' ' : val);
}
// Trim end whitespace.
output += line.replace('\\s+$/g', '') + '\n';
}
return output;
};
/**
* Loads the given text into the diagram starting at the given offset (centered).
* @param {string} value
* @param {ascii.Vector} offset
*/
ascii.State.prototype.fromText = function(value, offset) {
var lines = value.split('\n');
var middle = new ascii.Vector(0, Math.round(lines.length / 2));
for (var j = 0; j < lines.length; j++) {
middle.x = Math.max(middle.x, Math.round(lines[j].length / 2));
}
for (var j = 0; j < lines.length; j++) {
var line = lines[j];
for (var i = 0; i < line.length; i++) {
var char = line.charAt(i);
// Convert special output back to special chars.
// TODO: This is a horrible hack, need to handle multiple special chars
// correctly and preserve them through line drawing etc.
if (SPECIAL_VALUES.indexOf(char) != -1) {
char = SPECIAL_VALUE;
}
if (ALT_SPECIAL_VALUES.indexOf(char) != -1) {
char = ALT_SPECIAL_VALUE;
}
this.drawValue(new ascii.Vector(i, j).add(offset).subtract(middle), char);
}
}
};