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