Browse Source

Fix bad merge (#79)

* Fix bad merge

Some classes were still prefixed by ascii,
and constants not prefixed by c.

* More es6 changes and refactorings

Added helper methods to vector for creating vector from touch event and
mouse event. Added up, down, left, right helper methods.

* Refactor draw.js into one module per draw function
master
Brian Schlenker 2 years ago
parent
commit
8277399c61

+ 5
- 1
compile.sh View File

@@ -1,5 +1,9 @@
#!/bin/bash

java -client -jar closure-compiler.jar \
--js js-lib/*.js \
--js js-lib/**.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


+ 3
- 3
js-lib/common.js View File

@@ -20,12 +20,12 @@ export class Box {
/** type {number} */ this.endY = Math.max(a.y, b.y);
}

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

/** @return {Vector} */
/** @return {!Vector} */
bottomRight() {
return new Vector(this.endX, this.endY);
}
@@ -54,7 +54,7 @@ export class Cell {

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

/** @return {boolean} */

+ 2
- 2
js-lib/controller.js View File

@@ -2,7 +2,6 @@ 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,
@@ -11,7 +10,8 @@ import {
DrawErase,
DrawMove,
DrawText,
} from './draw';
DrawSelect,
} from './draw/index';


/**

+ 0
- 536
js-lib/draw.js View File

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

/**
* All drawing classes and functions.
*/

/**
* Draws a line on the diagram state.
*
* @param {State} state
* @param {Vector} startPosition
* @param {Vector} endPosition
* @param {boolean} clockwise
* @param {string=} value
*/
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;
var endY = box.endY;

var midX = clockwise ? endPosition.x : startPosition.x;
var midY = clockwise ? startPosition.y : endPosition.y;

while (startX++ < endX) {
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 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);
}
}

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

/**
* Common interface for different drawing functions, e.g. box, line, etc.
* @interface
*/
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) {};
}

/**
* @implements {DrawFunction}
*/
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) {};
}

/**
* @implements {DrawFunction}
*/
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) {};
}

/**
* @implements {DrawFunction}
*/
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;
}
}
}

/**
* @implements {DrawFunction}
*/
export class DrawText {
/**
* @param {State} state
*/
constructor(state, view) {
this.state = state;
this.startPosition = null;
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();});});
}
}

/** @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++;
}
}

/**
* 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);
}
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));
}
}

/**
* @implements {DrawFunction}
*/
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 */
end() {
this.state.commitDraw();
}

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

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

/**
* @implements {DrawFunction}
*/
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;
}
// 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});
}
}
}
this.ends = ends;
// Redraw the new lines after we have cleared the existing ones.
this.move(this.startPosition);
}

/** @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, ' ');
}
for (var i in this.ends) {
drawLine(this.state, position, end.position, end.clockwise);
}
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 */
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 {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;
}

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 {Vector} position
* @return {Vector}
*/
snapToNearest(position) {
if (this.state.getCell(position).isSpecial()) {
return position;
}
var allDirections = c.DIRECTIONS.concat([
c.DIR_LEFT.add(c.DIR_UP),
c.DIR_LEFT.add(c.DIR_DOWN),
c.DIR_RIGHT.add(c.DIR_UP),
c.DIR_RIGHT.add(c.DIR_DOWN)]);

var bestDirection = null;
var bestContextSum = 0;
for (var direction of allDirections) {
// Find the most connected cell, essentially.
var newPos = position.add(direction);
var contextSum = this.state.getContext(newPos).sum();
if (this.state.getCell(newPos).isSpecial() &&
contextSum > bestContextSum) {
bestDirection = direction;
bestContextSum = contextSum;
}
}
if (bestDirection == null) {
// Didn't find anything, so just return the current cell.
return position;
}
return position.add(bestDirection);
}

/** @inheritDoc */
getCursor(position) {
if (this.state.getCell(position).isSpecial()) {
return 'pointer';
} else {
return 'default';
}
}

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

+ 44
- 0
js-lib/draw/box.js View File

@@ -0,0 +1,44 @@
import State from '../state';
import Vector from '../vector';
import DrawFunction from './function';
import { drawLine } from './utils';

/**
* @implements {DrawFunction}
*/
export default 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) {};
}

+ 54
- 0
js-lib/draw/erase.js View File

@@ -0,0 +1,54 @@
import { DrawFunction } from './function';
import { ERASE_CHAR } from '../constants';
import State from '../state';
import Vector from '../vector';

/**
* @implements {DrawFunction}
*/
export default 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), ERASE_CHAR);
}
}
}

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

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

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

+ 54
- 0
js-lib/draw/freeform.js View File

@@ -0,0 +1,54 @@
import { DrawFunction } from './function';
import { TOUCH_ENABLED } from '../constants';
import State from '../state';

/**
* @implements {DrawFunction}
*/
export default class DrawFreeform {
/**
* @param {State} state
* @param {?string} value
*/
constructor(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 */
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 (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;
}
}
}

+ 21
- 0
js-lib/draw/function.js View File

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

/**
* Common interface for different drawing functions, e.g. box, line, etc.
* @interface
*/
export default 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) {};
}

+ 9
- 0
js-lib/draw/index.js View File

@@ -0,0 +1,9 @@
export { default as DrawFunction } from './function';
export { default as DrawBox } from './box';
export { default as DrawBoxText } from './boxtext';
export { default as DrawErase } from './erase';
export { default as DrawLine } from './line';
export { default as DrawSelect } from './select';
export { default as DrawText } from './text';
export { default as DrawMove } from './move';
export { default as DrawFreeform } from './freeform';

+ 55
- 0
js-lib/draw/line.js View File

@@ -0,0 +1,55 @@
import { DrawFunction } from './function';
import { drawLine } from './utils';
import { ALT_SPECIAL_VALUE } from '../constants';
import State from '../state';
import Vector from '../vector';

/**
* @implements {DrawFunction}
*/
export default 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, ALT_SPECIAL_VALUE);
}
}

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

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

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

+ 180
- 0
js-lib/draw/move.js View File

@@ -0,0 +1,180 @@
import { DrawFunction } from './function';
import { drawLine } from './utils';
import State from '../state';
import Vector from '../vector';
import * as c from '../constants';

/**
* @implements {DrawFunction}
*/
export default 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;
}
// 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});
}
}
}
this.ends = ends;
// Redraw the new lines after we have cleared the existing ones.
this.move(this.startPosition);
}

/** @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, ' ');
}
for (var i in this.ends) {
drawLine(this.state, position, end.position, end.clockwise);
}
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 */
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 {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;
}

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 {Vector} position
* @return {Vector}
*/
snapToNearest(position) {
if (this.state.getCell(position).isSpecial()) {
return position;
}
var allDirections = c.DIRECTIONS.concat([
c.DIR_LEFT.add(c.DIR_UP),
c.DIR_LEFT.add(c.DIR_DOWN),
c.DIR_RIGHT.add(c.DIR_UP),
c.DIR_RIGHT.add(c.DIR_DOWN)]);

var bestDirection = null;
var bestContextSum = 0;
for (var direction of allDirections) {
// Find the most connected cell, essentially.
var newPos = position.add(direction);
var contextSum = this.state.getContext(newPos).sum();
if (this.state.getCell(newPos).isSpecial() &&
contextSum > bestContextSum) {
bestDirection = direction;
bestContextSum = contextSum;
}
}
if (bestDirection == null) {
// Didn't find anything, so just return the current cell.
return position;
}
return position.add(bestDirection);
}

/** @inheritDoc */
getCursor(position) {
if (this.state.getCell(position).isSpecial()) {
return 'pointer';
} else {
return 'default';
}
}

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

js-lib/draw-select.js → js-lib/draw/select.js View File

@@ -1,8 +1,9 @@
import * as c from './constants';
import Vector from './vector';
import State from './state';
import { MappedValue, Box } from './common';
import { DrawFunction, DrawErase } from './draw';
import DrawFunction from './function';
import { ERASE_CHAR, KEY_COPY, KEY_CUT, KEY_PASTE } from '../constants';
import Vector from '../vector';
import State from '../state';
import { MappedValue, Box } from '../common';
import DrawErase from './erase';

/**
* @implements {DrawFunction}
@@ -53,7 +54,7 @@ export default class DrawSelect {
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;
return value.cell.getRawValue() != null && value.cell.getRawValue() != ERASE_CHAR;
});
var topLeft = this.getSelectedBox().topLeft();
this.selectedCells = nonEmptyCells.map(function(value) {
@@ -82,7 +83,7 @@ export default class DrawSelect {
// Effectively highlights the cell.
var currentValue = this.state.getCell(current).getRawValue();
this.state.drawValue(current,
currentValue == null ? c.ERASE_CHAR : currentValue);
currentValue == null ? ERASE_CHAR : currentValue);
}
}
}
@@ -129,17 +130,17 @@ export default class DrawSelect {
handleKey(value) {
if (this.startPosition != null &&
this.endPosition != null) {
if (value == c.KEY_COPY || value == c.KEY_CUT) {
if (value == KEY_COPY || value == KEY_CUT) {
this.copyArea();
}
if (value == c.KEY_CUT) {
if (value == KEY_CUT) {
var eraser = new DrawErase(this.state);
eraser.start(this.startPosition);
eraser.move(this.endPosition);
this.state.commitDraw();
}
}
if (value == c.KEY_PASTE) {
if (value == KEY_PASTE) {
this.drawSelected(this.startPosition);
this.state.commitDraw();
}

+ 102
- 0
js-lib/draw/text.js View File

@@ -0,0 +1,102 @@
import { DrawFunction } from './function';
import { ERASE_CHAR } from '../constants';
import State from '../state';
import Vector from '../vector';
import { drawText } from './utils';

/**
* @implements {DrawFunction}
*/
export default class DrawText {
/**
* @param {State} state
*/
constructor(state, view) {
this.state = state;
this.startPosition = null;
this.endPosition = null;
};

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

// TODO: 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 */
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, () => {
$('#text-tool-widget').show(0, () => {
$('#text-tool-input').focus();
});
});
}
}

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

/** @inheritDoc */
handleKey(value) {
var text = /** @type {string} */ ($('#text-tool-input').val());
this.state.clearDraw();
drawText(this.state, this.endPosition, text);
}

/**
* Loads any existing text if it is present.
* TODO: This is horrible, and does not quite work, fix it.
*/
loadExistingText(position) {
var currentPosition = position.clone();
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);
}
this.startPosition = currentPosition.right(spacesCount + 1);
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));
}
}

+ 67
- 0
js-lib/draw/utils.js View File

@@ -0,0 +1,67 @@
import { SPECIAL_VALUE } from '../constants';
import { Box } from '../common';
import State from '../state';
import Vector from '../vector';

/**
* Draws a line on the diagram state.
*
* @param {State} state
* @param {Vector} startPosition
* @param {Vector} endPosition
* @param {boolean} clockwise
* @param {string=} value
*/
export function drawLine(
state, startPosition, endPosition, clockwise, value = SPECIAL_VALUE) {

var box = new Box(startPosition, endPosition);
var startX = box.startX;
var startY = box.startY;
var endX = box.endX;
var endY = box.endY;

var midX = clockwise ? endPosition.x : startPosition.x;
var midY = clockwise ? startPosition.y : endPosition.y;

while (startX++ < endX) {
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 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);
}
}

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

/**
* Sets the cells scratch (uncommitted) values to the given text.
* Handles newlines appropriately.
* @param {State} state
* @param {Vector} position
* @param {string} text
*/
export function drawText(state, position, text) {
let x = 0, y = 0;
for (const char of text) {
if (char == '\n') {
y++;
x = 0;
continue;
}
state.drawValue(position.add(new Vector(x, y)), char);
x++;
}
}

+ 5
- 5
js-lib/drive-controller.js View File

@@ -42,7 +42,7 @@ export default class DriveController {

this.loopSave();

$(window).on('hashchange', this.loadFromHash);
$(window).on('hashchange', () => { this.loadFromHash(); });

$('#drive-new-file-button').click(() => {
this.file = null;
@@ -60,14 +60,14 @@ export default class DriveController {
window['gapi']['auth']['authorize']({
'client_id': CLIENT_ID,
'scope': SCOPES,
'immediate': immediate},
immediate },
result => {
if (result && !result.error && !this.driveEnabled) {
this.driveEnabled = true;
$('#drive-button').addClass('active');
// We are authorized, so let's se if we can load from the URL hash.
// This seems to fail if we do it too early.
window.setTimeout(this.loadFromHash, 500);
window.setTimeout(() => { this.loadFromHash(); }, 500);
}
});
}
@@ -136,7 +136,7 @@ export default class DriveController {
safeExecute(request, callback) {
// Could make the API call, don't blow up tho (mobiles n stuff).
try {
request['execute'](function(result) {
request['execute'](result => {
if (!result['error']) {
callback(result);
}
@@ -220,8 +220,8 @@ export default class DriveController {
var method = this.file == null ? 'POST' : 'PUT';

return window['gapi']['client']['request']({
method,
'path': '/upload/drive/v2/files' + fileId,
'method': method,
'params': {'uploadType': 'multipart'},
'headers': {
'Content-Type': 'multipart/mixed; boundary="' + boundary + '"'

+ 21
- 31
js-lib/input-controller.js View File

@@ -29,31 +29,31 @@ export class DesktopController {
* Installs input bindings associated with keyboard controls.
*/
installBindings() {
var canvas = this.controller.view.canvas;
$(canvas).on('mousewheel', e => {
var canvas = $(this.controller.view.canvas);
canvas.on('mousewheel', e => {
this.handleZoom(e.originalEvent.wheelDelta);
});

$(canvas).mousedown(e => {
canvas.mousedown(e => {
// Can drag by holding either the control or meta (Apple) key.
if (e.ctrlKey || e.metaKey) {
this.controller.startDrag(new Vector(e.clientX, e.clientY));
this.controller.startDrag(Vector.fromMouseEvent(e));
} else {
this.controller.startDraw(new Vector(e.clientX, e.clientY));
this.controller.startDraw(Vector.fromMouseEvent(e));
}
});

// Pass these events through to the main controller.
$(canvas).mouseup(e => {
canvas.mouseup(e => {
this.controller.endAll();
});

$(canvas).mouseleave(e => {
canvas.mouseleave(e => {
this.controller.endAll();
});

$(canvas).mousemove(e => {
this.controller.handleMove(new Vector(e.clientX, e.clientY));
canvas.mousemove(e => {
this.controller.handleMove(Vector.fromMouseEvent(e));
});
}
}
@@ -152,42 +152,32 @@ export class TouchController {
* Installs input bindings associated with touch controls.
*/
installBindings() {
var canvas = this.controller.view.canvas;
var canvas = $(this.controller.view.canvas);

$(canvas).on('touchstart', e => {
canvas.on('touchstart', e => {
e.preventDefault();
if (e.originalEvent.touches.length == 1) {
this.handlePress(new Vector(
e.originalEvent.touches[0].pageX,
e.originalEvent.touches[0].pageY));
this.handlePress(Vector.fromTouchEvent(e));
} else if (e.originalEvent.touches.length > 1) {
this.handlePressMulti(new Vector(
e.originalEvent.touches[0].pageX,
e.originalEvent.touches[0].pageY),
new Vector(
e.originalEvent.touches[1].pageX,
e.originalEvent.touches[1].pageY));
this.handlePressMulti(
Vector.fromTouchEvent(e, 0),
Vector.fromTouchEvent(e, 1));
}
});

$(canvas).on('touchmove', e => {
canvas.on('touchmove', e => {
e.preventDefault();
if (e.originalEvent.touches.length == 1) {
this.handleMove(new Vector(
e.originalEvent.touches[0].pageX,
e.originalEvent.touches[0].pageY));
this.handleMove(Vector.fromTouchEvent(e));
} else if (e.originalEvent.touches.length > 1) {
this.handleMoveMulti(new Vector(
e.originalEvent.touches[0].pageX,
e.originalEvent.touches[0].pageY),
new Vector(
e.originalEvent.touches[1].pageX,
e.originalEvent.touches[1].pageY));
this.handleMoveMulti(
Vector.fromTouchEvent(e, 0),
Vector.fromTouchEvent(e, 1));
}
});

// Pass through, no special handling.
$(canvas).on('touchend', e => {
canvas.on('touchend', e => {
e.preventDefault();
this.reset();
this.controller.endAll();

+ 24
- 26
js-lib/state.js View File

@@ -98,8 +98,8 @@ export default class State {
getDrawValue(position) {
var cell = this.getCell(position);
var value = cell.scratchValue != null ? cell.scratchValue : cell.value;
var isSpecial = c.SPECIAL_VALUES.indexOf(value) != -1;
var isAltSpecial = c.ALT_SPECIAL_VALUES.indexOf(value) != -1;
var isSpecial = c.SPECIAL_VALUES.includes(value);
var isAltSpecial = c.ALT_SPECIAL_VALUES.includes(value);
if (!isSpecial && !isAltSpecial) {
return value;
}
@@ -146,13 +146,13 @@ export default class State {
if (!context.up && context.rightdown && context.leftdown) {
return c.SPECIAL_LINE_H;
}
var leftupempty = this.getCell(position.add(c.DIR_LEFT).add(c.DIR_UP)).isEmpty();
var rightupempty = this.getCell(position.add(c.DIR_RIGHT).add(c.DIR_UP)).isEmpty();
var leftupempty = this.getCell(position.left().up()).isEmpty();
var rightupempty = this.getCell(position.right().up()).isEmpty();
if (context.up && context.left && context.right && (!leftupempty || !rightupempty)) {
return c.SPECIAL_LINE_H;
}
var leftdownempty = this.getCell(position.add(c.DIR_LEFT).add(c.DIR_DOWN)).isEmpty();
var rightdownempty = this.getCell(position.add(c.DIR_RIGHT).add(c.DIR_DOWN)).isEmpty();
var leftdownempty = this.getCell(position.left().down()).isEmpty();
var rightdownempty = this.getCell(position.right().down()).isEmpty();
if (context.down && context.left && context.right && (!leftdownempty || !rightdownempty)) {
return c.SPECIAL_LINE_H;
}
@@ -181,10 +181,10 @@ export default class State {
* @return {CellContext}
*/
getContext(position) {
var left = this.getCell(position.add(c.DIR_LEFT)).isSpecial();
var right = this.getCell(position.add(c.DIR_RIGHT)).isSpecial();
var up = this.getCell(position.add(c.DIR_UP)).isSpecial();
var down = this.getCell(position.add(c.DIR_DOWN)).isSpecial();
var left = this.getCell(position.left()).isSpecial();
var right = this.getCell(position.right()).isSpecial();
var up = this.getCell(position.up()).isSpecial();
var down = this.getCell(position.down()).isSpecial();
return new CellContext(left, right, up, down);
}

@@ -193,10 +193,10 @@ export default class State {
* @param {CellContext} context
*/
extendContext(position, context) {
context.leftup = this.getCell(position.add(c.DIR_LEFT).add(c.DIR_UP)).isSpecial();
context.rightup = this.getCell(position.add(c.DIR_RIGHT).add(c.DIR_UP)).isSpecial();
context.leftdown = this.getCell(position.add(c.DIR_LEFT).add(c.DIR_DOWN)).isSpecial();
context.rightdown = this.getCell(position.add(c.DIR_RIGHT).add(c.DIR_DOWN)).isSpecial();
context.leftup = this.getCell(position.left().up()).isSpecial();
context.rightup = this.getCell(position.right().up()).isSpecial();
context.leftdown = this.getCell(position.left().down()).isSpecial();
context.rightdown = this.getCell(position.right().down()).isSpecial();
}

/**
@@ -207,11 +207,10 @@ export default class State {
var oldValues = [];

// Dedupe the scratch values, or this causes havoc for history management.
var positions = this.scratchCells.map(function(value) {
var positions = this.scratchCells.map(value => {
return value.position.x.toString() + value.position.y.toString();
});
var scratchCellsUnique =
this.scratchCells.filter(function(value, index, arr) {
var scratchCellsUnique = this.scratchCells.filter((value, index, arr) => {
return positions.indexOf(positions[index]) == index;
});

@@ -253,9 +252,8 @@ export default class State {
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);
for (var { position, value } of lastState) {
this.drawValue(position, value);
}
this.commitDraw(true);
}
@@ -267,8 +265,8 @@ export default class State {
if (this.redoStates.length == 0) { return; }

var lastState = this.redoStates.pop();
for (var mappedValue of lastState) {
this.drawValue(mappedValue.position, mappedValue.value);
for (var { position, value } of lastState) {
this.drawValue(position, value);
}
this.commitDraw();
}
@@ -320,7 +318,7 @@ export default class State {
*/
fromText(value, offset) {
var lines = value.split('\n');
var middle = new ascii.Vector(0, Math.round(lines.length / 2));
var middle = new 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));
}
@@ -331,10 +329,10 @@ export default class State {
// 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 (c.SPECIAL_VALUES.includes(char)) {
char = c.SPECIAL_VALUE;
}
this.drawValue(new ascii.Vector(i, j).add(offset).subtract(middle), char);
this.drawValue(new Vector(i, j).add(offset).subtract(middle), char);
}
}
}

+ 56
- 2
js-lib/vector.js View File

@@ -12,7 +12,25 @@ export default class Vector {
}

/**
* @param {Vector} other
* @param {jQuery.Event} event
* @return {!Vector}
*/
static fromMouseEvent(event) {
return new Vector(event.clientX, event.clientY);
}

/**
* @param {jQuery.Event} event
* @param {number=} index
* @return {!Vector}
*/
static fromTouchEvent(event, index = 0) {
const { pageX, pageY } = event.originalEvent.touches[index];
return new Vector(pageX, pageY);
}

/**
* @param {?Vector} other
* @return {boolean}
*/
equals(other) {
@@ -36,7 +54,7 @@ export default class Vector {
}

/**
* @return {Vector}
* @return {!Vector}
*/
clone() {
return new Vector(this.x, this.y);
@@ -54,4 +72,40 @@ export default class Vector {
scale(scale) {
return new Vector(this.x * scale, this.y * scale);
}

/**
* Move up by value. Defaults to 1.
* @param {number=} value
* @return {Vector}
*/
up(value = 1) {
return new Vector(this.x, this.y - value);
}

/**
* Move down by value. Defaults to 1.
* @param {number=} value
* @return {Vector}
*/
down(value = 1) {
return new Vector(this.x, this.y + value);
}

/**
* Move left by value. Defaults to 1.
* @param {number=} value
* @return {Vector}
*/
left(value = 1) {
return new Vector(this.x - value, this.y);
}

/**
* Move right by value. Defaults to 1.
* @param {number=} value
* @return {Vector}
*/
right(value = 1) {
return new Vector(this.x + value, this.y);
}
}

+ 1
- 2
js-lib/view.js View File

@@ -45,8 +45,7 @@ export default class View {
this.state.dirty = false;
this.render();
}
var view = this;
window.requestAnimationFrame(function() { view.animate(); });
window.requestAnimationFrame(() => { this.animate(); });
}

/**

Loading…
Cancel
Save