Browse Source

Updated project to use es6 classes, arrow functions, latest closure-compiler, latest jquery (#77)

* Update jquer and closure compiler

* Move to es6 classes, arrow functions, for/of

* Move to es6 style modules

* Add .DS_Store to .gitignore

* Remove note to update compile.sh
master
Brian Schlenker 2 years ago
parent
commit
141b8eff92
19 changed files with 4739 additions and 2622 deletions
  1. 1
    0
      .gitignore
  2. 0
    1
      README
  3. BIN
      closure-compiler.jar
  4. 3
    11
      compile.sh
  5. 4
    4
      index.html
  6. 2045
    0
      jquery-3.1-externs.js
  7. 4
    0
      jquery-3.1.1.min.js
  8. 716
    706
      js-compiled.js
  9. 117
    218
      js-lib/common.js
  10. 51
    0
      js-lib/constants.js
  11. 243
    225
      js-lib/controller.js
  12. 121
    112
      js-lib/draw-select.js
  13. 444
    426
      js-lib/draw.js
  14. 223
    226
      js-lib/drive-controller.js
  15. 176
    170
      js-lib/input-controller.js
  16. 14
    10
      js-lib/launch.js
  17. 284
    283
      js-lib/state.js
  18. 57
    0
      js-lib/vector.js
  19. 236
    230
      js-lib/view.js

+ 1
- 0
.gitignore View File

@@ -3,3 +3,4 @@ closure-library/*
.settings/*
*~
_site/*
.DS_Store

+ 0
- 1
README View File

@@ -13,4 +13,3 @@ Goto: http://localhost:8000/index.html

When developing, use the Google JS linter, gjslint.

Adding new source files will require a change to compile.sh.

BIN
closure-compiler.jar View File


+ 3
- 11
compile.sh View File

@@ -1,13 +1,5 @@
java -client -jar closure-compiler.jar \
--js js-lib/common.js \
--js js-lib/view.js \
--js js-lib/draw.js \
--js js-lib/draw-select.js \
--js js-lib/state.js \
--js js-lib/controller.js \
--js js-lib/drive-controller.js \
--js js-lib/input-controller.js \
--js js-lib/launch.js \
--warning_level=VERBOSE --formatting=PRETTY_PRINT --language_in=ECMASCRIPT5 --compilation_level=ADVANCED_OPTIMIZATIONS \
--externs=jquery-1.9-externs.js \
--js js-lib/*.js \
--warning_level=VERBOSE --formatting=PRETTY_PRINT --language_in=ECMASCRIPT6 --compilation_level=ADVANCED_OPTIMIZATIONS \
--externs=jquery-3.1-externs.js \
> js-compiled.js

+ 4
- 4
index.html View File

@@ -90,7 +90,7 @@ button {
}

.info-description {
vertical-align: text-bottom;
vertical-align: text-bottom;
margin-left: 10px;
display: inline-block;
height: 30px;
@@ -141,7 +141,7 @@ button {
text-align: center;
padding-left: 0px;
}
/* Move file tools to the bottom. */
#file-tools {
left: 0px;
@@ -557,7 +557,7 @@ textarea {
<div class="dialog-button-bar">
<button class="close-dialog-button">Close</button>
<button id="import-submit-button">Import</button>
</div>
</div>
</div>

<!-- These dialogs are handled seperately. -->
@@ -587,7 +587,7 @@ textarea {

<canvas id="ascii-canvas"></canvas>

<script src="jquery-1.9.1.min.js"></script>
<script src="jquery-3.1.1.min.js"></script>
<script src="js-compiled.js"></script>
<script src="https://apis.google.com/js/client.js?onload=window.gapiCallback"></script>


+ 2045
- 0
jquery-3.1-externs.js
File diff suppressed because it is too large
View File


+ 4
- 0
jquery-3.1.1.min.js
File diff suppressed because it is too large
View File


+ 716
- 706
js-compiled.js
File diff suppressed because it is too large
View File


+ 117
- 218
js-lib/common.js View File

@@ -2,241 +2,140 @@
* Common classes and constants.
*/

// Define namespace for closure compiler but don't make it a requirement.
try {
goog.provide('ascii');
throw 1;
} catch (e) {
/** type {Object} */
window.ascii = window.ascii || {};
}

/** @const */ var MAX_GRID_WIDTH = 2000;
/** @const */ var MAX_GRID_HEIGHT = 600;

/** @const */ var SPECIAL_VALUE = '+';
/** @const */ var ALT_SPECIAL_VALUE = '^';
/** @const */ var SPECIAL_ARROW_LEFT = '<';
/** @const */ var SPECIAL_ARROW_UP = '^';
/** @const */ var SPECIAL_ARROW_RIGHT = '>';
/** @const */ var SPECIAL_ARROW_DOWN = 'v';
/** @const */ var SPECIAL_VALUES = ['+', '\u2012', '\u2013', '-', '|'];
/** @const */ var ALT_SPECIAL_VALUES = ['>', '<', '^', 'v'];
/** @const */ var ALL_SPECIAL_VALUES = SPECIAL_VALUES.concat(ALT_SPECIAL_VALUES);

/** @const */ var MAX_UNDO = 50;

/** @const */ var SPECIAL_LINE_H = '-';
/** @const */ var SPECIAL_LINE_V = '|';

/** @const */ var ERASE_CHAR = '\u2009';

/** @const */ var DRAG_LATENCY = 150; // Milliseconds.
/** @const */ var DRAG_ACCURACY = 6; // Pixels.

/** @const */ var CHAR_PIXELS_H = 9;
/** @const */ var CHAR_PIXELS_V = 17;

/** @const */ var RENDER_PADDING_CELLS = 3;

/** @const */ var KEY_RETURN = '<enter>';
/** @const */ var KEY_BACKSPACE = '<backspace>';
/** @const */ var KEY_COPY = '<copy>';
/** @const */ var KEY_PASTE = '<paste>';
/** @const */ var KEY_CUT = '<cut>';
/** @const */ var KEY_UP = '<up>';
/** @const */ var KEY_DOWN = '<down>';
/** @const */ var KEY_LEFT = '<left>';
/** @const */ var KEY_RIGHT = '<right>';

// http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript
/** @const */ var TOUCH_ENABLED =
'ontouchstart' in window ||
'onmsgesturechange' in window;

/**
* Stores a 2D vector.
*
* @constructor
* @param {number} x
* @param {number} y
*/
ascii.Vector = function(x, y) {
/** type {Number} */ this.x = x;
/** type {Number} */ this.y = y;
};

/**
* @param {ascii.Vector} other
* @return {boolean}
*/
ascii.Vector.prototype.equals = function(other) {
return (other != null) && (this.x == other.x) && (this.y == other.y);
};

/**
* @param {ascii.Vector} other
* @return {ascii.Vector}
*/
ascii.Vector.prototype.subtract = function(other) {
return new ascii.Vector(this.x - other.x, this.y - other.y);
};

/**
* @param {ascii.Vector} other
* @return {ascii.Vector}
*/
ascii.Vector.prototype.add = function(other) {
return new ascii.Vector(this.x + other.x, this.y + other.y);
};

/**
* @return {ascii.Vector}
*/
ascii.Vector.prototype.clone = function() {
return new ascii.Vector(this.x, this.y);
};

/** @return {number} */
ascii.Vector.prototype.length = function() {
return Math.sqrt(this.x * this.x + this.y * this.y);
};

/**
* @param {number} scale
* @return {ascii.Vector}
*/
ascii.Vector.prototype.scale = function(scale) {
return new ascii.Vector(this.x * scale, this.y * scale);
};
import { ERASE_CHAR, ALL_SPECIAL_VALUES } from './constants';
import Vector from './vector';

/**
* Represents a box with normalized position vectors.
*
* @constructor
* @param {ascii.Vector} a
* @param {ascii.Vector} b
*/
ascii.Box = function(a, b) {
/** type {Number} */ this.startX = Math.min(a.x, b.x);
/** type {Number} */ this.startY = Math.min(a.y, b.y);
/** type {Number} */ this.endX = Math.max(a.x, b.x);
/** type {Number} */ this.endY = Math.max(a.y, b.y);
};
/** @return {ascii.Vector} */
ascii.Box.prototype.topLeft = function() {
return new ascii.Vector(this.startX, this.startY);
};
/** @return {ascii.Vector} */
ascii.Box.prototype.bottomRight = function() {
return new ascii.Vector(this.endX, this.endY);
};
/** @return {boolean} */
ascii.Box.prototype.contains = function(position) {
return position.x >= this.startX && position.x <= this.endX && position.y >= this.startY && position.y <= this.endY;
};
/** @const */ var DIR_LEFT = new ascii.Vector(-1, 0);
/** @const */ var DIR_RIGHT = new ascii.Vector(1, 0);
/** @const */ var DIR_UP = new ascii.Vector(0, -1);
/** @const */ var DIR_DOWN = new ascii.Vector(0, 1);
/** @const */ var DIRECTIONS = [DIR_LEFT, DIR_RIGHT, DIR_UP, DIR_DOWN];
export class Box {
/**
* @param {Vector} a
* @param {Vector} b
*/
constructor(a, b) {
/** type {number} */ this.startX = Math.min(a.x, b.x);
/** type {number} */ this.startY = Math.min(a.y, b.y);
/** type {number} */ this.endX = Math.max(a.x, b.x);
/** type {number} */ this.endY = Math.max(a.y, b.y);
}

/** @return {Vector} */
topLeft() {
return new Vector(this.startX, this.startY);
}

/** @return {Vector} */
bottomRight() {
return new Vector(this.endX, this.endY);
}

/** @return {boolean} */
contains(position) {
return position.x >= this.startX && position.x <= this.endX
&& position.y >= this.startY && position.y <= this.endY;
}
}

/**
* An individual cell within the diagram and it's current value.
*
* @constructor
*/
ascii.Cell = function() {
/** @type {?string} */ this.value = null;
/** @type {?string} */ this.scratchValue = null;
};

/** @return {?string} */
ascii.Cell.prototype.getRawValue = function() {
return (this.scratchValue != null ? this.scratchValue : this.value);
};

/** @return {boolean} */
ascii.Cell.prototype.isSpecial = function() {
return ALL_SPECIAL_VALUES.indexOf(this.getRawValue()) != -1;
};

/** @return {boolean} */
ascii.Cell.prototype.isEmpty = function() {
return this.value == null && this.scratchValue == null;
};

/** @return {boolean} */
ascii.Cell.prototype.hasScratch = function() {
return this.scratchValue != null;
};

/** @return {boolean} */
ascii.Cell.prototype.isErase = function() {
return this.scratchValue == ERASE_CHAR;
};
export class Cell {

constructor() {
/** @type {?string} */ this.value = null;
/** @type {?string} */ this.scratchValue = null;
}

/** @return {?string} */
getRawValue() {
return (this.scratchValue != null ? this.scratchValue : this.value);
}

/** @return {boolean} */
isSpecial() {
return ALL_SPECIAL_VALUES.indexOf(this.getRawValue()) != -1;
}

/** @return {boolean} */
isEmpty() {
return this.value == null && this.scratchValue == null;
}

/** @return {boolean} */
hasScratch() {
return this.scratchValue != null;
}

/** @return {boolean} */
isErase() {
return this.scratchValue == ERASE_CHAR;
}
}

/**
* The context for a cell, i.e. the status of the cells around it.
*
* @param {boolean} left
* @param {boolean} right
* @param {boolean} up
* @param {boolean} down
* @constructor
*/
ascii.CellContext = function(left, right, up, down) {
/** @type {boolean} */ this.left = left;
/** @type {boolean} */ this.right = right;
/** @type {boolean} */ this.up = up;
/** @type {boolean} */ this.down = down;
/** @type {boolean} */ this.leftup = false;
/** @type {boolean} */ this.rightup = false;
/** @type {boolean} */ this.leftdown = false;
/** @type {boolean} */ this.rightdown = false;
};

/**
* Returns the total number of surrounding special cells.
* @return {number}
*/
ascii.CellContext.prototype.sum = function() {
return this.left + this.right + this.up + this.down;
};

/**
* Returns the total number of surrounding special cells.
* @return {number}
*/
ascii.CellContext.prototype.extendedSum = function() {
return this.left + this.right + this.up + this.down + this.leftup + this.leftdown + this.rightup + this.rightdown;
};
export class CellContext {
/**
* @param {boolean} left
* @param {boolean} right
* @param {boolean} up
* @param {boolean} down
*/
constructor(left, right, up, down) {
/** @type {boolean} */ this.left = left;
/** @type {boolean} */ this.right = right;
/** @type {boolean} */ this.up = up;
/** @type {boolean} */ this.down = down;
/** @type {boolean} */ this.leftup = false;
/** @type {boolean} */ this.rightup = false;
/** @type {boolean} */ this.leftdown = false;
/** @type {boolean} */ this.rightdown = false;
}

/**
* Returns the total number of surrounding special cells.
* @return {number}
*/
sum() {
return this.left + this.right + this.up + this.down;
}

/**
* Returns the total number of surrounding special cells.
* @return {number}
*/
extendedSum() {
return this.left + this.right + this.up + this.down
+ this.leftup + this.leftdown + this.rightup + this.rightdown;
}
}

/**
* A pair of a vector and a string value. Used in history management.
* @constructor
* @struct
* @param {ascii.Vector} position
* @param {string|null} value
*/
ascii.MappedValue = function(position, value) {
this.position = position;
this.value = value;
};
export class MappedValue {
/**
* @param {Vector} position
* @param {string|null} value
*/
constructor(position, value) {
this.position = position;
this.value = value;
}
}

/**
* A pair of a vector and a cell. Used in history management.
* @constructor
* @struct
* @param {ascii.Vector} position
* @param {ascii.Cell} cell
*/
ascii.MappedCell = function(position, cell) {
this.position = position;
this.cell = cell;
};
export class MappedCell {
/**
* @param {Vector} position
* @param {Cell} cell
*/
constructor(position, cell) {
this.position = position;
this.cell = cell;
}
}

+ 51
- 0
js-lib/constants.js View File

@@ -0,0 +1,51 @@
import Vector from './vector';

export const MAX_GRID_WIDTH = 2000;
export const MAX_GRID_HEIGHT = 600;

export const SPECIAL_VALUE = '+';
export const ALT_SPECIAL_VALUE = '^';
export const SPECIAL_ARROW_LEFT = '<';
export const SPECIAL_ARROW_UP = '^';
export const SPECIAL_ARROW_RIGHT = '>';
export const SPECIAL_ARROW_DOWN = 'v';
export const SPECIAL_VALUES = ['+', '\u2012', '\u2013', '-', '|'];
export const ALT_SPECIAL_VALUES = ['>', '<', '^', 'v'];
export const ALL_SPECIAL_VALUES = SPECIAL_VALUES.concat(ALT_SPECIAL_VALUES);

export const MAX_UNDO = 50;

export const SPECIAL_LINE_H = '-';
export const SPECIAL_LINE_V = '|';

export const ERASE_CHAR = '\u2009';

export const DRAG_LATENCY = 150; // Milliseconds.
export const DRAG_ACCURACY = 6; // Pixels.

export const CHAR_PIXELS_H = 9;
export const CHAR_PIXELS_V = 17;

export const RENDER_PADDING_CELLS = 3;

export const KEY_RETURN = '<enter>';
export const KEY_BACKSPACE = '<backspace>';
export const KEY_COPY = '<copy>';
export const KEY_PASTE = '<paste>';
export const KEY_CUT = '<cut>';
export const KEY_UP = '<up>';
export const KEY_DOWN = '<down>';
export const KEY_LEFT = '<left>';
export const KEY_RIGHT = '<right>';

// http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript
export const TOUCH_ENABLED =
'ontouchstart' in window ||
'onmsgesturechange' in window;

export const DIR_LEFT = new Vector(-1, 0);
export const DIR_RIGHT = new Vector( 1, 0);
export const DIR_UP = new Vector( 0, -1);
export const DIR_DOWN = new Vector( 0, 1);

export const DIRECTIONS = [DIR_LEFT, DIR_RIGHT, DIR_UP, DIR_DOWN];

+ 243
- 225
js-lib/controller.js View File

@@ -1,8 +1,23 @@
import * as c from './constants';
import Vector from './vector';
import View from './view';
import State from './state';
import DrawSelect from './draw-select';
import {
DrawFunction,
DrawBox,
DrawLine,
DrawFreeform,
DrawErase,
DrawMove,
DrawText,
} from './draw';


/**
* Different modes of control.
* @const
*/
var Mode = {
const Mode = {
NONE: 0,
DRAG: 1,
DRAW: 2
@@ -10,253 +25,256 @@ var Mode = {

/**
* Handles user input events and modifies state.
*
* @constructor
* @param {ascii.View} view
* @param {ascii.State} state
*/
ascii.Controller = function(view, state) {
/** @type {ascii.View} */ this.view = view;
/** @type {ascii.State} */ this.state = state;

/** @type {ascii.DrawFunction} */ this.drawFunction =
new ascii.DrawBox(state);

/** @type {number} */ this.mode = Mode.NONE;
/** @type {ascii.Vector} */ this.dragOrigin;
/** @type {ascii.Vector} */ this.dragOriginCell;
export default class Controller {
/**
* @param {View} view
* @param {State} state
*/
constructor(view, state) {
/** @type {View} */ this.view = view;
/** @type {State} */ this.state = state;

this.installBindings();
};

/**
* @param {ascii.Vector} position
*/
ascii.Controller.prototype.startDraw = function(position) {
this.mode = Mode.DRAW;
this.drawFunction.start(this.view.screenToCell(position));
};
/** @type {DrawFunction} */ this.drawFunction = new DrawBox(state);

/**
* @param {ascii.Vector} position
*/
ascii.Controller.prototype.startDrag = function(position) {
this.mode = Mode.DRAG;
this.dragOrigin = position;
this.dragOriginCell = this.view.offset;
};
/** @type {number} */ this.mode = Mode.NONE;
/** @type {Vector} */ this.dragOrigin;
/** @type {Vector} */ this.dragOriginCell;

/**
* @param {ascii.Vector} position
*/
ascii.Controller.prototype.handleMove = function(position) {
var moveCell = this.view.screenToCell(position);
/** @type {Vector} */ this.lastMoveCell = null;

// First move event, make sure we don't blow up here.
if (this.lastMoveCell == null) {
this.lastMoveCell = moveCell;
this.installBindings();
}

// Update the cursor pointer, depending on the draw function.
if (!moveCell.equals(this.lastMoveCell)) {
this.view.canvas.style.cursor = this.drawFunction.getCursor(moveCell);
/**
* @param {Vector} position
*/
startDraw(position) {
this.mode = Mode.DRAW;
this.drawFunction.start(this.view.screenToCell(position));
}

// In drawing mode, so pass the mouse move on, but remove duplicates.
if (this.mode == Mode.DRAW && !moveCell.equals(this.lastMoveCell)) {
this.drawFunction.move(moveCell);
/**
* @param {Vector} position
*/
startDrag(position) {
this.mode = Mode.DRAG;
this.dragOrigin = position;
this.dragOriginCell = this.view.offset;
}

// Drag in progress, update the view origin.
if (this.mode == Mode.DRAG) {
this.view.setOffset(this.dragOriginCell.add(
this.dragOrigin
.subtract(position)
.scale(1 / this.view.zoom)));
/**
* @param {Vector} position
*/
handleMove(position) {
var moveCell = this.view.screenToCell(position);

// First move event, make sure we don't blow up here.
if (this.lastMoveCell == null) {
this.lastMoveCell = moveCell;
}

// Update the cursor pointer, depending on the draw function.
if (!moveCell.equals(this.lastMoveCell)) {
this.view.canvas.style.cursor = this.drawFunction.getCursor(moveCell);
}

// In drawing mode, so pass the mouse move on, but remove duplicates.
if (this.mode == Mode.DRAW && !moveCell.equals(this.lastMoveCell)) {
this.drawFunction.move(moveCell);
}

// Drag in progress, update the view origin.
if (this.mode == Mode.DRAG) {
this.view.setOffset(this.dragOriginCell.add(
this.dragOrigin
.subtract(position)
.scale(1 / this.view.zoom)));
}
this.lastMoveCell = moveCell;
}
this.lastMoveCell = moveCell;
};

/**
* Ends the current operation.
*/
ascii.Controller.prototype.endAll = function() {
if (this.mode == Mode.DRAW) {
this.drawFunction.end();
/**
* Ends the current operation.
*/
endAll() {
if (this.mode == Mode.DRAW) {
this.drawFunction.end();
}
// Cleanup state.
this.mode = Mode.NONE;
this.dragOrigin = null;
this.dragOriginCell = null;
this.lastMoveCell = null;
}
// Cleanup state.
this.mode = Mode.NONE;
this.dragOrigin = null;
this.dragOriginCell = null;
this.lastMoveCell = null;
};

/**
* Installs input bindings for common use cases devices.
*/
ascii.Controller.prototype.installBindings = function() {
var controller = this;

$(window).resize(function(e) { controller.view.resizeCanvas() });

$('#draw-tools > button.tool').click(function(e) {
$('#text-tool-widget').hide(0);
this.handleDrawButton(e.target.id);
}.bind(this));

$('#file-tools > button.tool').click(function(e) {
this.handleFileButton(e.target.id);
}.bind(this));

$('button.close-dialog-button').click(function(e) {
$('.dialog').removeClass('visible');
}.bind(this));

$('#import-submit-button').click(function(e) {
this.state.clear();
this.state.fromText($('#import-area').val(),
this.view.screenToCell(new ascii.Vector(
this.view.canvas.width / 2,
this.view.canvas.height / 2)));
this.state.commitDraw();
$('#import-area').val('');
$('.dialog').removeClass('visible');
}.bind(this));
/**
* Installs input bindings for common use cases devices.
*/
installBindings() {
$(window).resize(e => { this.view.resizeCanvas() });

$('#draw-tools > button.tool').click(e => {
$('#text-tool-widget').hide(0);
this.handleDrawButton(e.target.id);
});

$('#file-tools > button.tool').click(e => {
this.handleFileButton(e.target.id);
});

$('button.close-dialog-button').click(e => {
$('.dialog').removeClass('visible');
});

$('#import-submit-button').click(e => {
this.state.clear();
this.state.fromText(
/** @type {string} */
($('#import-area').val()),
this.view.screenToCell(new Vector(
this.view.canvas.width / 2,
this.view.canvas.height / 2)));
this.state.commitDraw();
$('#import-area').val('');
$('.dialog').removeClass('visible');
});

$('#use-lines-button').click(e => {
$('.dialog').removeClass('visible');
this.view.setUseLines(true);
});

$('#use-ascii-button').click(e => {
$('.dialog').removeClass('visible');
this.view.setUseLines(false);
});

$(window).keypress(e => {
this.handleKeyPress(e);
});

$(window).keydown(e => {
this.handleKeyDown(e);
});

// Bit of a hack, just triggers the text tool to get a new value.
$('#text-tool-input, #freeform-tool-input').keyup(() => {
this.drawFunction.handleKey('');
});
$('#text-tool-input, #freeform-tool-input').change(() => {
this.drawFunction.handleKey('');
});
$('#text-tool-close').click(() => {
$('#text-tool-widget').hide();
this.state.commitDraw();
});
}

$('#use-lines-button').click(function(e) {
/**
* Handles the buttons in the UI.
* @param {string} id The ID of the element clicked.
*/
handleDrawButton(id) {
$('#draw-tools > button.tool').removeClass('active');
$('#' + id).toggleClass('active');
$('.dialog').removeClass('visible');
this.view.setUseLines(true);
}.bind(this));

$('#use-ascii-button').click(function(e) {
$('.dialog').removeClass('visible');
this.view.setUseLines(false);
}.bind(this));

$(window).keypress(function(e) {
this.handleKeyPress(e);
}.bind(this));

$(window).keydown(function(e) {
this.handleKeyDown(e);
}.bind(this));

// Bit of a hack, just triggers the text tool to get a new value.
$('#text-tool-input, #freeform-tool-input').keyup(function(){
this.drawFunction.handleKey('');
}.bind(this));
$('#text-tool-input, #freeform-tool-input').change(function(){
this.drawFunction.handleKey('');
}.bind(this));
$('#text-tool-close').click(function(){
$('#text-tool-widget').hide();
// Install the right draw tool based on button pressed.
if (id == 'box-button') {
this.drawFunction = new DrawBox(this.state);
}
if (id == 'line-button') {
this.drawFunction = new DrawLine(this.state, false);
}
if (id == 'arrow-button') {
this.drawFunction = new DrawLine(this.state, true);
}
if (id == 'freeform-button') {
this.drawFunction = new DrawFreeform(this.state, "X");
}
if (id == 'erase-button') {
this.drawFunction = new DrawErase(this.state);
}
if (id == 'move-button') {
this.drawFunction = new DrawMove(this.state);
}
if (id == 'text-button') {
this.drawFunction = new DrawText(this.state, this.view);
}
if (id == 'select-button') {
this.drawFunction = new DrawSelect(this.state);
}
this.state.commitDraw();
}.bind(this));
};

/**
* Handles the buttons in the UI.
* @param {string} id The ID of the element clicked.
*/
ascii.Controller.prototype.handleDrawButton = function(id) {
$('#draw-tools > button.tool').removeClass('active');
$('#' + id).toggleClass('active');
$('.dialog').removeClass('visible');

// Install the right draw tool based on button pressed.
if (id == 'box-button') {
this.drawFunction = new ascii.DrawBox(this.state);
}
if (id == 'line-button') {
this.drawFunction = new ascii.DrawLine(this.state, false);
}
if (id == 'arrow-button') {
this.drawFunction = new ascii.DrawLine(this.state, true);
}
if (id == 'freeform-button') {
this.drawFunction = new ascii.DrawFreeform(this.state, "X");
}
if (id == 'erase-button') {
this.drawFunction = new ascii.DrawErase(this.state);
}
if (id == 'move-button') {
this.drawFunction = new ascii.DrawMove(this.state);
}
if (id == 'text-button') {
this.drawFunction = new ascii.DrawText(this.state, this.view);
}
if (id == 'select-button') {
this.drawFunction = new ascii.DrawSelect(this.state);
}
this.state.commitDraw();
this.view.canvas.focus();
};

/**
* Handles the buttons in the UI.
* @param {string} id The ID of the element clicked.
*/
ascii.Controller.prototype.handleFileButton = function(id) {
$('.dialog').removeClass('visible');
$('#' + id + '-dialog').toggleClass('visible');

if (id == 'import-button') {
$('#import-area').val('');
$('#import-area').focus();
}

if (id == 'export-button') {
$('#export-area').val(this.state.outputText());
$('#export-area').select();
this.view.canvas.focus();
}
if (id == 'clear-button') {
this.state.clear();
}
if (id == 'undo-button') {
this.state.undo();
}
if (id == 'redo-button') {
this.state.redo();
}
};

/**
* Handles key presses.
* @param {Object} event
*/
ascii.Controller.prototype.handleKeyPress = function(event) {
if (!event.ctrlKey && !event.metaKey && event.keyCode != 13) {
this.drawFunction.handleKey(String.fromCharCode(event.keyCode));
/**
* Handles the buttons in the UI.
* @param {string} id The ID of the element clicked.
*/
handleFileButton(id) {
$('.dialog').removeClass('visible');
$('#' + id + '-dialog').toggleClass('visible');

if (id == 'import-button') {
$('#import-area').val('');
$('#import-area').focus();
}

if (id == 'export-button') {
$('#export-area').val(this.state.outputText());
$('#export-area').select();
}
if (id == 'clear-button') {
this.state.clear();
}
if (id == 'undo-button') {
this.state.undo();
}
if (id == 'redo-button') {
this.state.redo();
}
}
};

/**
* Handles key down events.
* @param {Object} event
*/
ascii.Controller.prototype.handleKeyDown = function(event) {
// Override some special characters so that they can be handled in one place.
var specialKeyCode = null;

if (event.ctrlKey || event.metaKey) {
if (event.keyCode == 67) { specialKeyCode = KEY_COPY; }
if (event.keyCode == 86) { specialKeyCode = KEY_PASTE; }
if (event.keyCode == 90) { this.state.undo(); }
if (event.keyCode == 89) { this.state.redo(); }
if (event.keyCode == 88) { specialKeyCode = KEY_CUT; }
/**
* Handles key presses.
* @param {jQuery.Event} event
*/
handleKeyPress(event) {
if (!event.ctrlKey && !event.metaKey && event.keyCode != 13) {
this.drawFunction.handleKey(String.fromCharCode(event.keyCode));
}
}

if (event.keyCode == 8) { specialKeyCode = KEY_BACKSPACE; }
if (event.keyCode == 13) { specialKeyCode = KEY_RETURN; }
if (event.keyCode == 38) { specialKeyCode = KEY_UP; }
if (event.keyCode == 40) { specialKeyCode = KEY_DOWN; }
if (event.keyCode == 37) { specialKeyCode = KEY_LEFT; }
if (event.keyCode == 39) { specialKeyCode = KEY_RIGHT; }

if (specialKeyCode != null) {
//event.preventDefault();
//event.stopPropagation();
this.drawFunction.handleKey(specialKeyCode);
/**
* Handles key down events.
* @param {jQuery.Event} event
*/
handleKeyDown(event) {
// Override some special characters so that they can be handled in one place.
var specialKeyCode = null;

if (event.ctrlKey || event.metaKey) {
if (event.keyCode == 67) { specialKeyCode = c.KEY_COPY; }
if (event.keyCode == 86) { specialKeyCode = c.KEY_PASTE; }
if (event.keyCode == 90) { this.state.undo(); }
if (event.keyCode == 89) { this.state.redo(); }
if (event.keyCode == 88) { specialKeyCode = c.KEY_CUT; }
}

if (event.keyCode == 8) { specialKeyCode = c.KEY_BACKSPACE; }
if (event.keyCode == 13) { specialKeyCode = c.KEY_RETURN; }
if (event.keyCode == 38) { specialKeyCode = c.KEY_UP; }
if (event.keyCode == 40) { specialKeyCode = c.KEY_DOWN; }
if (event.keyCode == 37) { specialKeyCode = c.KEY_LEFT; }
if (event.keyCode == 39) { specialKeyCode = c.KEY_RIGHT; }

if (specialKeyCode != null) {
//event.preventDefault();
//event.stopPropagation();
this.drawFunction.handleKey(specialKeyCode);
}
}
};
}


+ 121
- 112
js-lib/draw-select.js View File

@@ -1,138 +1,147 @@
import * as c from './constants';
import Vector from './vector';
import State from './state';
import { MappedValue, Box } from './common';
import { DrawFunction, DrawErase } from './draw';

/**
* @constructor
* @implements {ascii.DrawFunction}
* @param {ascii.State} state
* @implements {DrawFunction}
*/
ascii.DrawSelect = function(state) {
this.state = state;
/** @type {ascii.Vector} */
this.startPosition = null;
/** @type {ascii.Vector} */
this.endPosition = null;
/** @type {ascii.Vector} */
this.dragStart = null;
/** @type {ascii.Vector} */
this.dragEnd = null;

/** @type {boolean} */
this.finished = true;
export default class DrawSelect {
/**
* @param {State} state
*/
constructor(state) {
this.state = state;
/** @type {Vector} */
this.startPosition = null;
/** @type {Vector} */
this.endPosition = null;
/** @type {Vector} */
this.dragStart = null;
/** @type {Vector} */
this.dragEnd = null;

/** @type {Array.<ascii.MappedValue>} */
this.selectedCells = null;
};
/** @type {boolean} */
this.finished = true;

/** @inheritDoc */
ascii.DrawSelect.prototype.start = function(position) {
// Must be dragging.
if (this.startPosition != null &&
this.endPosition != null &&
this.getSelectedBox().contains(position)) {
this.dragStart = position;
this.copyArea();
this.dragMove(position);
} else {
this.startPosition = position;
this.endPosition = null;
this.finished = false;
this.move(position);
/** @type {!Array<MappedValue>} */
this.selectedCells = [];
}
};

ascii.DrawSelect.prototype.getSelectedBox = function() {
return new ascii.Box(this.startPosition, this.endPosition);
};

ascii.DrawSelect.prototype.copyArea = function() {
var nonEmptyCells = this.state.scratchCells.filter(function(value) {
var rawValue = value.cell.getRawValue();
return value.cell.getRawValue() != null && value.cell.getRawValue() != ERASE_CHAR;
});
var topLeft = this.getSelectedBox().topLeft();
this.selectedCells = nonEmptyCells.map(function(value) {
return new ascii.MappedValue(value.position.subtract(topLeft), value.cell.getRawValue());
});
};
/** @inheritDoc */
start(position) {
// Must be dragging.
if (this.startPosition != null &&
this.endPosition != null &&
this.getSelectedBox().contains(position)) {
this.dragStart = position;
this.copyArea();
this.dragMove(position);
} else {
this.startPosition = position;
this.endPosition = null;
this.finished = false;
this.move(position);
}
}

/** @inheritDoc */
ascii.DrawSelect.prototype.move = function(position) {
if (this.dragStart != null) {
this.dragMove(position);
return;
getSelectedBox() {
return new Box(this.startPosition, this.endPosition);
}

if (this.finished == true) {
return;
copyArea() {
var nonEmptyCells = this.state.scratchCells.filter(function(value) {
var rawValue = value.cell.getRawValue();
return value.cell.getRawValue() != null && value.cell.getRawValue() != c.ERASE_CHAR;
});
var topLeft = this.getSelectedBox().topLeft();
this.selectedCells = nonEmptyCells.map(function(value) {
return new MappedValue(value.position.subtract(topLeft), value.cell.getRawValue());
});
}
this.endPosition = position;
this.state.clearDraw();

var box = new ascii.Box(this.startPosition, position);
/** @inheritDoc */
move(position) {
if (this.dragStart != null) {
this.dragMove(position);
return;
}

if (this.finished == true) {
return;
}
this.endPosition = position;
this.state.clearDraw();

var box = new Box(this.startPosition, position);

for (var i = box.startX; i <= box.endX; i++) {
for (var j = box.startY; j <= box.endY; j++) {
var current = new ascii.Vector(i, j);
// Effectively highlights the cell.
var currentValue = this.state.getCell(current).getRawValue();
this.state.drawValue(current,
currentValue == null ? ERASE_CHAR : currentValue);
for (var i = box.startX; i <= box.endX; i++) {
for (var j = box.startY; j <= box.endY; j++) {
var current = new Vector(i, j);
// Effectively highlights the cell.
var currentValue = this.state.getCell(current).getRawValue();
this.state.drawValue(current,
currentValue == null ? c.ERASE_CHAR : currentValue);
}
}
}
};

ascii.DrawSelect.prototype.dragMove = function(position) {
this.dragEnd = position;
this.state.clearDraw();
var eraser = new ascii.DrawErase(this.state);
eraser.start(this.startPosition);
eraser.move(this.endPosition);
var startPos = this.dragEnd.subtract(this.dragStart).add(this.getSelectedBox().topLeft());
this.drawSelected(startPos);
};
dragMove(position) {
this.dragEnd = position;
this.state.clearDraw();
var eraser = new DrawErase(this.state);
eraser.start(this.startPosition);
eraser.move(this.endPosition);
var startPos = this.dragEnd.subtract(this.dragStart).add(this.getSelectedBox().topLeft());
this.drawSelected(startPos);
}

ascii.DrawSelect.prototype.drawSelected = function(startPos) {
for (var i in this.selectedCells) {
this.state.drawValue(this.selectedCells[i].position.add(startPos), this.selectedCells[i].value);
drawSelected(startPos) {
for (var { position, value } of this.selectedCells) {
this.state.drawValue(position.add(startPos), value);
}
}
};

/** @inheritDoc */
ascii.DrawSelect.prototype.end = function() {
if (this.dragStart != null) {
this.state.commitDraw();
this.startPosition = null;
this.endPosition = null;
/** @inheritDoc */
end() {
if (this.dragStart != null) {
this.state.commitDraw();
this.startPosition = null;
this.endPosition = null;
}
this.dragStart = null;
this.dragEnd = null;
this.finished = true;
}
this.dragStart = null;
this.dragEnd = null;
this.finished = true;
};

/** @inheritDoc */
ascii.DrawSelect.prototype.getCursor = function(position) {
if (this.startPosition != null &&
this.endPosition != null &&
new ascii.Box(this.startPosition, this.endPosition).contains(position)) {
return 'pointer';
/** @inheritDoc */
getCursor(position) {
if (this.startPosition != null &&
this.endPosition != null &&
new Box(this.startPosition, this.endPosition).contains(position)) {
return 'pointer';
}
return 'default';
}
return 'default';
};

/** @inheritDoc */
ascii.DrawSelect.prototype.handleKey = function(value) {
if (this.startPosition != null &&
this.endPosition != null) {
if (value == KEY_COPY || value == KEY_CUT) {
this.copyArea();
/** @inheritDoc */
handleKey(value) {
if (this.startPosition != null &&
this.endPosition != null) {
if (value == c.KEY_COPY || value == c.KEY_CUT) {
this.copyArea();
}
if (value == c.KEY_CUT) {
var eraser = new DrawErase(this.state);
eraser.start(this.startPosition);
eraser.move(this.endPosition);
this.state.commitDraw();
}
}
if (value == KEY_CUT) {
var eraser = new ascii.DrawErase(this.state);
eraser.start(this.startPosition);
eraser.move(this.endPosition);
if (value == c.KEY_PASTE) {
this.drawSelected(this.startPosition);
this.state.commitDraw();
}
}
if (value == KEY_PASTE) {
this.drawSelected(this.startPosition);
this.state.commitDraw();
}
};
}

+ 444
- 426
js-lib/draw.js View File

@@ -1,3 +1,8 @@
import * as c from './constants';
import State from './state';
import Vector from './vector';
import { Box } from './common';

/**
* All drawing classes and functions.
*/
@@ -5,16 +10,14 @@
/**
* Draws a line on the diagram state.
*
* @param {ascii.State} state
* @param {ascii.Vector} startPosition
* @param {ascii.Vector} endPosition
* @param {State} state
* @param {Vector} startPosition
* @param {Vector} endPosition
* @param {boolean} clockwise
* @param {string=} opt_value
* @param {string=} value
*/
function drawLine(state, startPosition, endPosition, clockwise, opt_value) {
var value = opt_value || SPECIAL_VALUE;

var box = new ascii.Box(startPosition, endPosition);
function drawLine(state, startPosition, endPosition, clockwise, value = c.SPECIAL_VALUE) {
var box = new Box(startPosition, endPosition);
var startX = box.startX;
var startY = box.startY;
var endX = box.endX;
@@ -24,16 +27,16 @@ function drawLine(state, startPosition, endPosition, clockwise, opt_value) {
var midY = clockwise ? startPosition.y : endPosition.y;

while (startX++ < endX) {
var position = new ascii.Vector(startX, midY);
var context = state.getContext(new ascii.Vector(startX, midY));
var position = new Vector(startX, midY);
var context = state.getContext(new Vector(startX, midY));
// Don't erase any lines that we cross.
if (value != ' ' || context.up + context.down != 2) {
state.drawValueIncremental(position, value);
}
}
while (startY++ < endY) {
var position = new ascii.Vector(midX, startY);
var context = state.getContext(new ascii.Vector(midX, startY));
var position = new Vector(midX, startY);
var context = state.getContext(new Vector(midX, startY));
// Don't erase any lines that we cross.
if (value != ' ' || context.left + context.right != 2) {
state.drawValueIncremental(position, value);
@@ -42,477 +45,492 @@ function drawLine(state, startPosition, endPosition, clockwise, opt_value) {

state.drawValue(startPosition, value);
state.drawValue(endPosition, value);
state.drawValueIncremental(new ascii.Vector(midX, midY), value);
state.drawValueIncremental(new Vector(midX, midY), value);
}

/**
* Common interface for different drawing functions, e.g. box, line, etc.
* @interface
*/
ascii.DrawFunction = function() {};
/** Start of drawing. @param {ascii.Vector} position */
ascii.DrawFunction.prototype.start = function(position) {};
/** Drawing move. @param {ascii.Vector} position */
ascii.DrawFunction.prototype.move = function(position) {};
/** End of drawing. */
ascii.DrawFunction.prototype.end = function() {};
/** Cursor for given cell.
* @param {ascii.Vector} position
* @return {string}
*/
ascii.DrawFunction.prototype.getCursor = function(position) {};
/** Handle the key with given value being pressed. @param {string} value */
ascii.DrawFunction.prototype.handleKey = function(value) {};
export class DrawFunction {
/** Start of drawing. @param {Vector} position */
start(position) {};
/** Drawing move. @param {Vector} position */
move(position) {};
/** End of drawing. */
end() {};
/** Cursor for given cell.
* @param {Vector} position
* @return {string}
*/
getCursor(position) {};
/** Handle the key with given value being pressed. @param {string} value */
handleKey(value) {};
}

/**
* @constructor
* @implements {ascii.DrawFunction}
* @param {ascii.State} state
* @implements {DrawFunction}
*/
ascii.DrawBox = function(state) {
this.state = state;
/** @type {ascii.Vector} */ this.startPosition = null;
};

/** @inheritDoc */
ascii.DrawBox.prototype.start = function(position) {
this.startPosition = position;
};

/** @inheritDoc */
ascii.DrawBox.prototype.move = function(position) {
this.endPosition = position;
this.state.clearDraw();
drawLine(this.state, this.startPosition, position, true);
drawLine(this.state, this.startPosition, position, false);
};

/** @inheritDoc */
ascii.DrawBox.prototype.end = function() {
this.state.commitDraw();
};

/** @inheritDoc */
ascii.DrawBox.prototype.getCursor = function(position) {
return 'crosshair';
};

/** @inheritDoc */
ascii.DrawBox.prototype.handleKey = function(value) {};
export class DrawBox {
/**
* @param {State} state
*/
constructor(state) {
this.state = state;
/** @type {Vector} */ this.startPosition = null;
/** @type {Vector} */ this.endPosition = null;
}

/** @inheritDoc */
start(position) {
this.startPosition = position;
}

/** @inheritDoc */
move(position) {
this.endPosition = position;
this.state.clearDraw();
drawLine(this.state, this.startPosition, position, true);
drawLine(this.state, this.startPosition, position, false);
}

/** @inheritDoc */
end() {
this.state.commitDraw();
}

/** @inheritDoc */
getCursor(position) {
return 'crosshair';
}

/** @inheritDoc */
handleKey(value) {};
}

/**
* @constructor
* @implements {ascii.DrawFunction}
* @param {ascii.State} state
* @param {boolean} isArrow
* @implements {DrawFunction}
*/
ascii.DrawLine = function(state, isArrow) {
this.state = state;
this.isArrow = isArrow;
/** @type {ascii.Vector} */ this.startPosition = null;
};

/** @inheritDoc */
ascii.DrawLine.prototype.start = function(position) {
this.startPosition = position;
};

/** @inheritDoc */
ascii.DrawLine.prototype.move = function(position) {
this.state.clearDraw();

// Try to infer line orientation.
// TODO: Split the line into two lines if we can't satisfy both ends.
var startContext = this.state.getContext(this.startPosition);
var endContext = this.state.getContext(position);
var clockwise = (startContext.up && startContext.down) ||
(endContext.left && endContext.right);

drawLine(this.state, this.startPosition, position, clockwise);
if (this.isArrow) {
this.state.drawValue(position, ALT_SPECIAL_VALUE);
}
};

/** @inheritDoc */
ascii.DrawLine.prototype.end = function() {
this.state.commitDraw();
};

/** @inheritDoc */
ascii.DrawLine.prototype.getCursor = function(position) {
return 'crosshair';
};

/** @inheritDoc */
ascii.DrawLine.prototype.handleKey = function(value) {};
export class DrawLine {
/**
* @param {State} state
* @param {boolean} isArrow
*/
constructor(state, isArrow) {
this.state = state;
this.isArrow = isArrow;
/** @type {Vector} */ this.startPosition = null;
}

/** @inheritDoc */
start(position) {
this.startPosition = position;
}

/** @inheritDoc */
move(position) {
this.state.clearDraw();

// Try to infer line orientation.
// TODO: Split the line into two lines if we can't satisfy both ends.
var startContext = this.state.getContext(this.startPosition);
var endContext = this.state.getContext(position);
var clockwise = (startContext.up && startContext.down) ||
(endContext.left && endContext.right);

drawLine(this.state, this.startPosition, position, clockwise);
if (this.isArrow) {
this.state.drawValue(position, c.ALT_SPECIAL_VALUE);
}
}

/** @inheritDoc */
end() {
this.state.commitDraw();
}

/** @inheritDoc */
getCursor(position) {
return 'crosshair';
}

/** @inheritDoc */
handleKey(value) {};
}

/**
* @constructor
* @implements {ascii.DrawFunction}
* @param {ascii.State} state
* @param {?string} value
* @implements {DrawFunction}
*/
ascii.DrawFreeform = function(state, value) {
this.state = state;
this.value = value;
if (TOUCH_ENABLED) {
$('#freeform-tool-input').val('');
$('#freeform-tool-input').hide(0, function() {$('#freeform-tool-input').show(0, function() {$('#freeform-tool-input').focus();});});
}
};

/** @inheritDoc */
ascii.DrawFreeform.prototype.start = function(position) {
this.state.drawValue(position, this.value);
};

/** @inheritDoc */
ascii.DrawFreeform.prototype.move = function(position) {
this.state.drawValue(position, this.value);
};

/** @inheritDoc */
ascii.DrawFreeform.prototype.end = function() {
this.state.commitDraw();
};

/** @inheritDoc */
ascii.DrawFreeform.prototype.getCursor = function(position) {
return 'crosshair';
};

/** @inheritDoc */
ascii.DrawFreeform.prototype.handleKey = function(value) {
if (TOUCH_ENABLED) {
this.value = $('#freeform-tool-input').val().substr(0, 1);
$('#freeform-tool-input').blur();
$('#freeform-tool-input').hide(0);
}
if (value.length == 1) {
// The value is not a special character, so lets use it.
export class DrawFreeform {
/**
* @param {State} state
* @param {?string} value
*/
constructor(state, value) {
this.state = state;
this.value = value;
if (c.TOUCH_ENABLED) {
$('#freeform-tool-input').val('');
$('#freeform-tool-input').hide(0, function() {$('#freeform-tool-input').show(0, function() {$('#freeform-tool-input').focus();});});
}
}

/** @inheritDoc */
start(position) {
this.state.drawValue(position, this.value);
}

/** @inheritDoc */
move(position) {
this.state.drawValue(position, this.value);
}

/** @inheritDoc */
end() {
this.state.commitDraw();
}

/** @inheritDoc */
getCursor(position) {
return 'crosshair';
}
};

/** @inheritDoc */
handleKey(value) {
if (c.TOUCH_ENABLED) {
this.value = $('#freeform-tool-input').val().substr(0, 1);
$('#freeform-tool-input').blur();
$('#freeform-tool-input').hide(0);
}
if (value.length == 1) {
// The value is not a special character, so lets use it.
this.value = value;
}
}
}

/**
* @constructor
* @implements {ascii.DrawFunction}
* @param {ascii.State} state
* @implements {DrawFunction}
*/
ascii.DrawText = function(state, view) {
this.state = state;
this.startPosition = null;
};

/** @inheritDoc */
ascii.DrawText.prototype.start = function(position) {
this.state.commitDraw();
$('#text-tool-input').val('');
this.startPosition = position;

// Not working yet, needs fixing so that it can remove the underlying text completely.
//this.loadExistingText(position);

// Effectively highlights the starting cell.
var currentValue = this.state.getCell(this.startPosition).getRawValue();
this.state.drawValue(this.startPosition,
currentValue == null ? ERASE_CHAR : currentValue);
};

/** @inheritDoc */
ascii.DrawText.prototype.move = function(position) {};

/** @inheritDoc */
ascii.DrawText.prototype.end = function() {
if (this.startPosition != null) {
this.endPosition = this.startPosition;
export class DrawText {
/**
* @param {State} state
*/
constructor(state, view) {
this.state = state;
this.startPosition = null;
// Valid end click/press, show the textbox and focus it.
$('#text-tool-widget').hide(0, function() {$('#text-tool-widget').show(0, function() {$('#text-tool-input').focus();});});
}
};

/** @inheritDoc */
ascii.DrawText.prototype.getCursor = function(position) {
return 'pointer';
};

/** @inheritDoc */
ascii.DrawText.prototype.handleKey = function(value) {
var text = $('#text-tool-input').val();
this.state.clearDraw();
var x = 0, y = 0;
for(var i = 0; i < text.length; i++) {
if (text[i] == '\n') {
y++;
x = 0;
continue;
this.endPosition = null;
};

/** @inheritDoc */
start(position) {
this.state.commitDraw();
$('#text-tool-input').val('');
this.startPosition = position;

// Not working yet, needs fixing so that it can remove the underlying text completely.
//this.loadExistingText(position);

// Effectively highlights the starting cell.
var currentValue = this.state.getCell(this.startPosition).getRawValue();
this.state.drawValue(this.startPosition,
currentValue == null ? c.ERASE_CHAR : currentValue);
}

/** @inheritDoc */
move(position) {}

/** @inheritDoc */
end() {
if (this.startPosition != null) {
this.endPosition = this.startPosition;
this.startPosition = null;
// Valid end click/press, show the textbox and focus it.
$('#text-tool-widget').hide(0, function() {$('#text-tool-widget').show(0, function() {$('#text-tool-input').focus();});});
}
this.state.drawValue(this.endPosition.add(new ascii.Vector(x, y)), text[i]);
x++;
}
};

/**
* Loads any existing text if it is present.
* TODO: This is horrible, and does not quite work, fix it.
*/
ascii.DrawText.prototype.loadExistingText = function(position) {
var currentPosition = new ascii.Vector(position.x, position.y);
var cell = this.state.getCell(position);
var spacesCount = 0;
// Go back to find the start of the line.
while ((!cell.isSpecial() && cell.getRawValue() != null) || spacesCount < 1) {
if (cell.getRawValue() == null) {
spacesCount++;
} else if (!cell.isSpecial()) {
spacesCount = 0;
/** @inheritDoc */
getCursor(position) {
return 'pointer';
}

/** @inheritDoc */
handleKey(value) {
var text = $('#text-tool-input').val();
this.state.clearDraw();
var x = 0, y = 0;
for(var i = 0; i < text.length; i++) {
if (text[i] == '\n') {
y++;
x = 0;
continue;
}
this.state.drawValue(this.endPosition.add(new Vector(x, y)), text[i]);
x++;
}
currentPosition.x--;
cell = this.state.getCell(currentPosition);
}
this.startPosition = currentPosition.add(new ascii.Vector(spacesCount + 1, 0));
var text = '';
spacesCount = 0;
currentPosition = this.startPosition.clone();
// Go forward to load the text.
while ((!cell.isSpecial() && cell.getRawValue() != null) || spacesCount < 1) {
cell = this.state.getCell(currentPosition);
if (cell.getRawValue() == null) {
spacesCount++;
text += ' ';
} else if (!cell.isSpecial()) {
spacesCount = 0;
text += cell.getRawValue();
this.state.drawValue(currentPosition, cell.getRawValue());
}

/**
* Loads any existing text if it is present.
* TODO: This is horrible, and does not quite work, fix it.
*/
loadExistingText(position) {
var currentPosition = new Vector(position.x, position.y);
var cell = this.state.getCell(position);
var spacesCount = 0;
// Go back to find the start of the line.
while ((!cell.isSpecial() && cell.getRawValue() != null) || spacesCount < 1) {
if (cell.getRawValue() == null) {
spacesCount++;
} else if (!cell.isSpecial()) {
spacesCount = 0;
}
currentPosition.x--;
cell = this.state.getCell(currentPosition);
}
currentPosition.x++;
this.startPosition = currentPosition.add(new Vector(spacesCount + 1, 0));
var text = '';
spacesCount = 0;
currentPosition = this.startPosition.clone();
// Go forward to load the text.
while ((!cell.isSpecial() && cell.getRawValue() != null) || spacesCount < 1) {
cell = this.state.getCell(currentPosition);
if (cell.getRawValue() == null) {
spacesCount++;
text += ' ';
} else if (!cell.isSpecial()) {
spacesCount = 0;
text += cell.getRawValue();
this.state.drawValue(currentPosition, cell.getRawValue());
}
currentPosition.x++;
}
$('#text-tool-input').val(text.substr(0, text.length - 1));
}
$('#text-tool-input').val(text.substr(0, text.length - 1));
};
}

/**
* @constructor
* @implements {ascii.DrawFunction}
* @param {ascii.State} state
* @implements {DrawFunction}
*/
ascii.DrawErase = function(state) {
this.state = state;
this.startPosition = null;
this.endPosition = null;
};

/** @inheritDoc */
ascii.DrawErase.prototype.start = function(position) {
this.startPosition = position;
this.move(position);
};

/** @inheritDoc */
ascii.DrawErase.prototype.move = function(position) {
this.state.clearDraw();
this.endPosition = position;

var startX = Math.min(this.startPosition.x, this.endPosition.x);
var startY = Math.min(this.startPosition.y, this.endPosition.y);
var endX = Math.max(this.startPosition.x, this.endPosition.x);
var endY = Math.max(this.startPosition.y, this.endPosition.y);

for (var i = startX; i <= endX; i++) {
for (var j = startY; j <= endY; j++) {
this.state.drawValue(new ascii.Vector(i, j), ERASE_CHAR);
export class DrawErase {
/**
* @param {State} state
*/
constructor(state) {
this.state = state;
this.startPosition = null;
this.endPosition = null;
}

/** @inheritDoc */
start(position) {
this.startPosition = position;
this.move(position);
}

/** @inheritDoc */
move(position) {
this.state.clearDraw();
this.endPosition = position;

var startX = Math.min(this.startPosition.x, this.endPosition.x);
var startY = Math.min(this.startPosition.y, this.endPosition.y);
var endX = Math.max(this.startPosition.x, this.endPosition.x);
var endY = Math.max(this.startPosition.y, this.endPosition.y);

for (var i = startX; i <= endX; i++) {
for (var j = startY; j <= endY; j++) {
this.state.drawValue(new Vector(i, j), c.ERASE_CHAR);
}
}
}
};

/** @inheritDoc */
ascii.DrawErase.prototype.end = function() {
this.state.commitDraw();
};
/** @inheritDoc */
end() {
this.state.commitDraw();
}

/** @inheritDoc */
ascii.DrawErase.prototype.getCursor = function(position) {
return 'crosshair';
};
/** @inheritDoc */
getCursor(position) {
return 'crosshair';
}

/** @inheritDoc */
ascii.DrawErase.prototype.handleKey = function(value) {};
/** @inheritDoc */
handleKey(value) {}
}

/**
* @constructor
* @implements {ascii.DrawFunction}
* @param {ascii.State} state
* @implements {DrawFunction}
*/
ascii.DrawMove = function(state) {
this.state = state;
this.startPosition = null;
this.ends = null;
};

/** @inheritDoc */
ascii.DrawMove.prototype.start = function(position) {
this.startPosition =
TOUCH_ENABLED ? this.snapToNearest(position) : position;
this.ends = null;

// If this isn't a special cell then quit, or things get weird.
if (!this.state.getCell(this.startPosition).isSpecial()) {
return;
}
var context = this.state.getContext(this.startPosition);

var ends = [];
for (var i in DIRECTIONS) {
var midPoints = this.followLine(this.startPosition, DIRECTIONS[i]);
for (var k in midPoints) {
var midPoint = midPoints[k];

// Clockwise is a lie, it is true if we move vertically first.
var clockwise = (DIRECTIONS[i].x != 0);
var startIsAlt = ALT_SPECIAL_VALUES.indexOf(this.state.getCell(position).getRawValue()) != -1;
var midPointIsAlt = ALT_SPECIAL_VALUES.indexOf(this.state.getCell(midPoint).getRawValue()) != -1;

var midPointContext = this.state.getContext(midPoint);
// Special case, a straight line with no turns.
if (midPointContext.sum() == 1) {
ends.push({position: midPoint, clockwise: clockwise, startIsAlt: startIsAlt, endIsAlt: midPointIsAlt});
continue;
}
// Continue following lines from the midpoint.
for (var j in DIRECTIONS) {
if (DIRECTIONS[i].add(DIRECTIONS[j]).length() == 0 ||
DIRECTIONS[i].add(DIRECTIONS[j]).length() == 2) {
// Don't go back on ourselves, or don't carry on in same direction.
export class DrawMove {
/**
* @param {State} state
*/
constructor(state) {
this.state = state;
this.startPosition = null;
/** @type {!Array<{position, clockwise, startIsAlt, midPointIsAlt, endIsAlt}>} */
this.ends = [];
}

/** @inheritDoc */
start(position) {
this.startPosition =
c.TOUCH_ENABLED ? this.snapToNearest(position) : position;
this.ends = [];

// If this isn't a special cell then quit, or things get weird.
if (!this.state.getCell(this.startPosition).isSpecial()) {
return;
}
var context = this.state.getContext(this.startPosition);

var ends = [];
for (var i of c.DIRECTIONS) {
var midPoints = this.followLine(this.startPosition, i);
for (var midPoint of midPoints) {
// Clockwise is a lie, it is true if we move vertically first.
var clockwise = (i.x != 0);
var startIsAlt = c.ALT_SPECIAL_VALUES.indexOf(this.state.getCell(position).getRawValue()) != -1;
var midPointIsAlt = c.ALT_SPECIAL_VALUES.indexOf(this.state.getCell(midPoint).getRawValue()) != -1;

var midPointContext = this.state.getContext(midPoint);
// Special case, a straight line with no turns.
if (midPointContext.sum() == 1) {
ends.push({position: midPoint, clockwise, startIsAlt, endIsAlt: midPointIsAlt});
continue;
}
var secondEnds = this.followLine(midPoint, DIRECTIONS[j]);
// Ignore any directions that didn't go anywhere.
if (secondEnds.length == 0) {
continue;
// Continue following lines from the midpoint.
for (var j of c.DIRECTIONS) {
if (i.add(j).length() == 0 || i.add(j).length() == 2) {
// Don't go back on ourselves, or don't carry on in same direction.
continue;
}
var secondEnds = this.followLine(midPoint, j);
// Ignore any directions that didn't go anywhere.
if (secondEnds.length == 0) {
continue;
}
var secondEnd = secondEnds[0];
var endIsAlt = c.ALT_SPECIAL_VALUES.indexOf(this.state.getCell(secondEnd).getRawValue()) != -1;
// On the second line we don't care about multiple
// junctions, just the last.
ends.push({position: secondEnd, clockwise, startIsAlt, midPointIsAlt, endIsAlt});
}
var secondEnd = secondEnds[0];
var endIsAlt = ALT_SPECIAL_VALUES.indexOf(this.state.getCell(secondEnd).getRawValue()) != -1;
// On the second line we don't care about multiple
// junctions, just the last.
ends.push({position: secondEnd,
clockwise: clockwise, startIsAlt: startIsAlt, midPointIsAlt: midPointIsAlt, endIsAlt: endIsAlt});
}
}
this.ends = ends;
// Redraw the new lines after we have cleared the existing ones.
this.move(this.startPosition);
}
this.ends = ends;
// Redraw the new lines after we have cleared the existing ones.
this.move(this.startPosition);
};

/** @inheritDoc */
ascii.DrawMove.prototype.move = function(position) {
this.state.clearDraw();
// Clear all the lines so we can draw them afresh.
for (var i in this.ends) {
drawLine(this.state, this.startPosition, this.ends[i].position,
this.ends[i].clockwise, ' ');
}
for (var i in this.ends) {
drawLine(this.state, position, this.ends[i].position,
this.ends[i].clockwise);
}
for (var i in this.ends) {
// If the ends or midpoint of the line was a alt character (arrow), need to preserve that.
if (this.ends[i].startIsAlt) {
this.state.drawValue(position, ALT_SPECIAL_VALUE);

/** @inheritDoc */
move(position) {
this.state.clearDraw();
// Clear all the lines so we can draw them afresh.
for (var end of this.ends) {
drawLine(this.state, this.startPosition, end.position, end.clockwise, ' ');
}
if (this.ends[i].endIsAlt) {
this.state.drawValue(this.ends[i].position, ALT_SPECIAL_VALUE);
for (var i in this.ends) {
drawLine(this.state, position, end.position, end.clockwise);
}
if (this.ends[i].midPointIsAlt) {
var midX = this.ends[i].clockwise ? this.ends[i].position.x : position.x;
var midY = this.ends[i].clockwise ? position.y : this.ends[i].position.y;
this.state.drawValue(new ascii.Vector(midX, midY), ALT_SPECIAL_VALUE);
for (var end of this.ends) {
// If the ends or midpoint of the line was a alt character (arrow), need to preserve that.
if (end.startIsAlt) {
this.state.drawValue(position, c.ALT_SPECIAL_VALUE);
}
if (end.endIsAlt) {
this.state.drawValue(end.position, c.ALT_SPECIAL_VALUE);
}
if (end.midPointIsAlt) {
var midX = end.clockwise ? end.position.x : position.x;
var midY = end.clockwise ? position.y : end.position.y;
this.state.drawValue(new Vector(midX, midY), c.ALT_SPECIAL_VALUE);
}
}
}
};

/** @inheritDoc */
ascii.DrawMove.prototype.end = function() {
this.state.commitDraw();
};
/** @inheritDoc */
end() {
this.state.commitDraw();
}

/**
* Follows a line in a given direction from the startPosition.
* Returns a list of positions that were line 'junctions'. This is a bit of a
* loose definition, but basically means a point around which we resize things.
* @param {ascii.Vector} startPosition
* @param {ascii.Vector} direction
* @return {Array.<ascii.Vector>}
*/
ascii.DrawMove.prototype.followLine = function(startPosition, direction) {
var endPosition = startPosition.clone();
var junctions = [];
while (true) {
var nextEnd = endPosition.add(direction);
if (!this.state.getCell(nextEnd).isSpecial()) {
// Junctions: Right angles and end T-Junctions.
if (!startPosition.equals(endPosition)) {
junctions.push(endPosition);
/**
* Follows a line in a given direction from the startPosition.
* Returns a list of positions that were line 'junctions'. This is a bit of a
* loose definition, but basically means a point around which we resize things.
* @param {Vector} startPosition
* @param {Vector} direction
* @return {!Array<Vector>}
*/
followLine(startPosition, direction) {
var endPosition = startPosition.clone();
var junctions = [];
while (true) {
var nextEnd = endPosition.add(direction);
if (!this.state.getCell(nextEnd).isSpecial()) {
// Junctions: Right angles and end T-Junctions.
if (!startPosition.equals(endPosition)) {
junctions.push(endPosition);
}
return junctions;
}
return junctions;
}

endPosition = nextEnd;
var context = this.state.getContext(endPosition);
// Junctions: Side T-Junctions.
if (context.sum() == 3) {
junctions.push(endPosition);
endPosition = nextEnd;
var context = this.state.getContext(endPosition);
// Junctions: Side T-Junctions.
if (context.sum() == 3) {
junctions.push(endPosition);
}
}
}
};

/**
* For a given position, finds the nearest cell that is of any interest to the
* move tool, e.g. a corner or a line. Will look up to 1 cell in each direction
* including diagonally.
* @param {ascii.Vector} position
* @return {ascii.Vector}
*/
ascii.DrawMove.prototype.snapToNearest = function(position) {
if (this.state.getCell(position).isSpecial()) {
return position;
}
var allDirections = DIRECTIONS.concat([
DIR_LEFT.add(DIR_UP),
DIR_LEFT.add(DIR_DOWN),
DIR_RIGHT.add(DIR_UP),
DIR_RIGHT.add(DIR_DOWN)]);

var bestDirection = null;
var bestContextSum = 0;
for (var i in allDirections) {