diff --git a/compile.sh b/compile.sh index 3a41a4a..173a12a 100755 --- a/compile.sh +++ b/compile.sh @@ -105,7 +105,7 @@ done # v= needs to be changed on every push. Find a better way to do this, e.g. # originated from the server once this is all rendered in Soy. cat main.html | sed -e $sedcommand | \ - sed -e 's#dist/runlocal.js#static/code.js?v=7#' | \ + sed -e 's#dist/runlocal.js#static/code.js?v=8#' | \ sed -e 's#"static/#"/static/#g' | \ html-minifier --remove-comments --collapse-whitespace --minify-css \ > build.html diff --git a/main.html b/main.html index ff4f10f..380711c 100644 --- a/main.html +++ b/main.html @@ -77,6 +77,14 @@ width: 32px; height: 32px; } +.colorButton { + min-width: 48px; + margin: 0; + vertical-align: top; +} +.colorButton.isSelected { + border: 2px solid black; +} .gridSquare { margin: 4px; display: inline-block; @@ -198,8 +206,9 @@ .tcell { min-width: 32px; min-height: 32px; - margin: 0 3px; + margin: 3px 3px; background-color: gray; + vertical-align: top; } .tcell.yellow { background-color: yellow; diff --git a/src/app.js b/src/app.js index 1e74554..d985812 100644 --- a/src/app.js +++ b/src/app.js @@ -319,12 +319,12 @@ var EditorCtrl = function( } $scope['neg'] = false; $scope['origin'] = window.location.origin; - // May be nullable. var initializeGridData = function() { if ($grid.isGridInitialized()) { - var dim = $grid.getDimensions(); - $scope['w'] = dim.width; - $scope['h'] = dim.height; + var init = $grid.getInitialState(); + $scope['w'] = init.width; + $scope['h'] = init.height; + $scope['symmetry'] = init.symmetry; } // If initialized from hash, manually close pane. // With sunsetting of static simulator, this has changed @@ -347,6 +347,8 @@ var EditorCtrl = function( 'hexagon', 'disjoint' ]; + // Should match grid.proto. + // In theory could derive a reverse mapping here. $scope['ccodes'] = { 1: 'black', 2: 'white', @@ -355,8 +357,15 @@ var EditorCtrl = function( 5: 'yellow', 6: 'red', 7: 'green', - 8: 'blue' - } + 8: 'blue', + 9: 'orange' + }; + $scope['scodes'] = { + 1: 'No symmetry', + 2: 'Horizontal symmetry', + 3: 'Vertical symmetry', + 4: 'Rotational symmetry' + }; // Some utilities. var safeInt = function(i) { i = parseInt(i); @@ -390,6 +399,9 @@ var EditorCtrl = function( $scope.getSelectClass = function(n) { return n == $scope['obj'] ? 'isSelected' : null; } + $scope.getSelectColorClass = function(n) { + return n == $scope['color'] ? 'isSelected' : null; + } $scope.selectColor = function(n) { $scope['color'] = n; } @@ -429,7 +441,8 @@ var EditorCtrl = function( grid: tetris['g'], gridWidth: 5, gridFixed: tetris['f'], - count: $scope['count'] + count: $scope['count'], + symmetry: safeInt($scope['symmetry']) }; $grid.setEditEntity(data); } else if ($mdMedia('gt-sm')) { @@ -443,7 +456,7 @@ var EditorCtrl = function( var tetris = $scope['tetris'][$scope['neg']]; return [ $scope['obj'], $scope['color'], $scope['w'], $scope['h'], - tetris['f'], $scope['neg'], $scope['count'] + tetris['f'], $scope['neg'], $scope['count'], $scope['symmetry'] ].join('.') }, alertGrid); $scope.$watch(function() { @@ -514,6 +527,7 @@ var EditorCtrl = function( // Timeout necessary because the grid can't just be rendered, it has to // be in the document, and the controller is initiated before document // inclusion. + // TODO: Revisit this given recent Grid/GridRenderer refactor. setTimeout(function() { $grid.render(); if (!gridAvailable) { @@ -1030,9 +1044,13 @@ GridService.prototype.getSolvePath = function() { return grid.solvedPuzzleVersion == grid.solvedPuzzleVersion ? grid.solvedPuzzlePath : null; } -GridService.prototype.getDimensions = function() { +GridService.prototype.getInitialState = function() { var grid = goog.asserts.assert(this.currentGrid()); - return {width: grid.grid.width, height: grid.grid.height}; + return { + width: grid.grid.width, + height: grid.grid.height, + symmetry: grid.grid.symmetry + }; } GridService.prototype.setEditEntity = function(opt_data) { var grid = goog.asserts.assert(this.currentGrid()); @@ -1101,7 +1119,7 @@ var Config = function( window.location.href = path; } $mdIconProvider - .iconSet('witness', '/static/witness.svg?v=0', 100) + .iconSet('witness', '/static/witness.svg?v=2', 100) $mdIconProvider .icon('basic', '/static/basic.svg', 550) diff --git a/src/build.tmpl.html b/src/build.tmpl.html index b690cf0..67efaa8 100644 --- a/src/build.tmpl.html +++ b/src/build.tmpl.html @@ -34,10 +34,12 @@
+ ng-click="selectColor(id); $event.stopPropagation()" + ng-class="getSelectColorClass(id)">
- Fixed position + Fixed orientation
@@ -82,6 +84,14 @@ +
+
+ + + {{name}} + + +

See FAQ for feedback and remaining bugs.

diff --git a/src/grid.js b/src/grid.js index da92eb9..d826271 100644 --- a/src/grid.js +++ b/src/grid.js @@ -8,6 +8,7 @@ var GridProto = windmill.GridProto; var Entity = GridProto.Entity; var Shape = GridProto.Shape; var Type = GridProto.Type; +var SymmetryType = GridProto.SymmetryType; var Color = GridProto.Color; var Storage = GridProto.Storage; var Orientation = GridProto.Orientation; @@ -72,11 +73,14 @@ Grid.prototype.initialize = function(width, height, opt_data) { // Grid state. if (storage) { this.entities = storage.entity; + this.symmetry = storage.symmetry || SymmetryType.NONE; + this.sanitize(); } else { this.entities = []; for (var i = 0; i < this.storeWidth * this.storeHeight; i++) { this.entities[i] = new Entity(); } + this.symmetry = SymmetryType.NONE; // Some freebies. // TODO: More configurations. this.pointEntity(0, this.height, new Entity(Type.START)); @@ -192,7 +196,11 @@ Grid.prototype.getHash = function() { entities.push(e); } }, this); - var encode = new Storage(this.storeWidth, entities).encode64(); + var storage = new Storage(this.storeWidth, entities); + if (this.symmetry && this.symmetry != SymmetryType.NONE) { + storage.symmetry = this.symmetry; + } + var encode = storage.encode64(); // Make it more URL-safe. // Note that this only affects 0x3E and 0x3F, which does not occur // much in protobufs due to how it encodes things. @@ -294,6 +302,39 @@ Grid.prototype.forEachEntity = function(fn, opt_scope) { } } } +Grid.prototype.setSymmetry = function(symmetry) { + this.symmetry = symmetry; + this.sanitize(); +} +Grid.prototype.sanitize = function() { + var sym = this.getSymmetry(); + if (!sym) { + return; + } + this.forEachEntity(function(value, i, j, drawType) { + if (drawType == DrawType.POINT && + (value.type == Type.START || value.type == Type.END)) { + var ref = sym.reflectPoint({i: i, j: j}); + var refValue = this.pointEntity(ref.i, ref.j); + if (value.type != refValue.type) { + if (value.type == Type.END) { + value = new Entity(value); + value.orientation = this.getEndPlacement(ref.i, ref.j); + } + this.pointEntity(ref.i, ref.j, value); + } + } + }, this); +} +Grid.prototype.getSymmetry = function() { + if (this.symmetry == SymmetryType.NONE) { + return null; + } else { + return new Grid.Symmetry(this.symmetry, this.width, this.height); + } +} +// Returns the automatic end orientation at a coord i, j. +// This is symmetrical for all coordinates and symmetries. Grid.prototype.getEndPlacement = function(i, j) { for (var di = -1; di <= 1; di += 2) { var line = this.lineBetweenEntity(i, j, i + di, j); @@ -310,4 +351,37 @@ Grid.prototype.getEndPlacement = function(i, j) { return null; } +/** @constructor */ +Grid.Symmetry = function(type, width, height) { + this.type = type; + this.width = width; + this.height = height; +} +Grid.Symmetry.prototype.reflectPoint = function(coord) { + if (this.type == SymmetryType.HORIZONTAL) { + return {i: this.width - coord.i, j: coord.j}; + } else if (this.type == SymmetryType.VERTICAL) { + return {i: coord.i, j: this.height - coord.j}; + } else if (this.type == SymmetryType.ROTATIONAL) { + return {i: this.width - coord.i, j: this.height - coord.j}; + } else { + throw Error(this.type); + } +} +Grid.Symmetry.prototype.reflectDelta = function(delta) { + // Avoid introducing negative zero here. + var negate = function(n) { + return n == 0 ? n : -n; + } + if (this.type == SymmetryType.HORIZONTAL) { + return {di: negate(delta.di), dj: delta.dj}; + } else if (this.type == SymmetryType.VERTICAL) { + return {di: delta.di, dj: negate(delta.dj)}; + } else if (this.type == SymmetryType.ROTATIONAL) { + return {di: negate(delta.di), dj: negate(delta.dj)}; + } else { + throw Error(this.type); + } +} + }); diff --git a/src/grid.proto b/src/grid.proto index 104fed2..6d73542 100644 --- a/src/grid.proto +++ b/src/grid.proto @@ -52,20 +52,31 @@ enum Color { RED = 6; GREEN = 7; BLUE = 8; + ORANGE = 9; +} +enum SymmetryType { + UNKNOWN = 0; + NONE = 1; + HORIZONTAL = 2; + VERTICAL = 3; + ROTATIONAL = 4; } message Storage { int32 width = 1; repeated Entity entity = 2; + optional SymmetryType symmetry = 3; } // Non-wire enums. enum DrawType { - CELL = 0; - POINT = 1; - HLINE = 2; - VLINE = 3; + UNKNOWN = 0; + CELL = 1; + POINT = 2; + HLINE = 3; + VLINE = 4; } enum SegmentType { - START = 0; - MIDDLE = 1; - END = 2; + UNKNOWN = 0; + START = 1; + MIDDLE = 2; + END = 3; } diff --git a/src/grouping.js b/src/grouping.js index c5be341..e4bfda5 100644 --- a/src/grouping.js +++ b/src/grouping.js @@ -16,12 +16,15 @@ var coordKey = windmill.keys.coordKey; var lineKey = windmill.keys.lineKey; /** @constructor */ -windmill.Path = function(coords, width, height) { +windmill.Path = function(coords, width, height, opt_newPaths) { // TODO: Also take in a grid shape, to enable non-standard grids. this.coords = coords; this.coordsMap = goog.array.toObject(coords, coordKey); var lines = []; for (var i = 1; i < coords.length; i++) { + if (opt_newPaths && goog.array.contains(opt_newPaths, i)) { + continue; + } var c1 = coords[i-1]; var c2 = coords[i]; lines.push({ diff --git a/src/snake.js b/src/snake.js index 0fb4868..e5c5326 100644 --- a/src/snake.js +++ b/src/snake.js @@ -15,11 +15,26 @@ var SegmentType = GridProto.SegmentType; var UI = windmill.constants.UI; + /** @constructor */ -windmill.Snake = function(start, draw, opt_mouseCoords) { - this.snakeId = Snake.snakeId_++; +windmill.Snake = function(start, draw, opt_mouseCoords, opt_symmetry) { // The current path. + this.snakeId = Snake.snakeId_++; this.movement = [start]; + // Symmetry snakes (render-only) + if (opt_symmetry) { + this.symmetry = opt_symmetry; + this.secondarySnakeId = Snake.snakeId_++; + this.secondaryMovement = [this.symmetry.reflectPoint(start)]; + } else { + this.symmetry = null; + this.secondarySnakeId = null; + this.secondaryMovement = null; + } + // SVG elements + this.snakeEl = null; + this.secondarySnakeEl = null; + // Progress through path this.target = null; this.targetMaxProgress = null; this.targetIsEnd = null; @@ -34,7 +49,6 @@ windmill.Snake = function(start, draw, opt_mouseCoords) { this.mouseX = opt_mouseCoords ? opt_mouseCoords.x : 0; this.mouseY = opt_mouseCoords ? opt_mouseCoords.y : 0; this.frameTime = new ElapsedTime(); - this.mouseTime = new ElapsedTime(); this.mouseHistoryX = []; this.mouseHistoryY = []; this.targetingMouse = !!opt_mouseCoords; @@ -58,16 +72,19 @@ Snake.prototype.markSuccessful = function() { 'filter', 'url(' + window.location.href.split('#')[0] + '#glow)'); } + if (this.secondarySnakeEl) { + this.secondarySnakeEl.style.setProperty( + 'filter', + 'url(' + window.location.href.split('#')[0] + '#glow)'); + } } Snake.prototype.setMouse = function(mouseX, mouseY) { this.mouseX = mouseX; this.mouseY = mouseY; - this.mouseTime.step(); } Snake.prototype.setMouseDiff = function(mouseX, mouseY) { this.mouseX += mouseX; this.mouseY += mouseY; - this.mouseTime.step(); } Snake.prototype.calcMouseOnGrid = function() { var gridOnPage = goog.style.getPageOffset( @@ -111,9 +128,6 @@ Snake.prototype.moveTowardsMouse = function(msPerGridUnit, selector) { this.mouseHistoryX.shift(); this.mouseHistoryY.shift(); } - if (!this.mouseTime.lastStep) { - return; - } // Icky... should ideally separate max distance into vertical and horizontal. var mdx = (this.mouseHistoryX[this.mouseHistoryX.length-1]-this.mouseHistoryX[0])/this.mouseHistoryX.length*2.5; @@ -265,6 +279,9 @@ Snake.prototype.moveTowardsTarget = function(maxMovement, selector) { throw Error(); } this.movement.push(this.target); + if (this.symmetry) { + this.secondaryMovement.push(this.symmetry.reflectPoint(this.target)); + } this.clearTarget(); this.progress = 0; // console.log('Forwards, target null!'); @@ -287,7 +304,11 @@ Snake.prototype.discoverTarget = function(selector, params) { var previous = this.movement.length > 1 ? this.movement[this.movement.length - 2] : null; var response = selector.selectTarget( - this.movement, params.di, params.dj, params.preferHorizontal); + params.di, + params.dj, + params.preferHorizontal, + this.movement, + this.secondaryMovement); var select = response.select; // Allow the case where select is absent or equal to previous value. if (!select || (select.i == current.i && select.j == current.j)) { @@ -306,6 +327,9 @@ Snake.prototype.discoverTarget = function(selector, params) { } if (previous && (select.i == previous.i && select.j == previous.j)) { this.movement.pop(); + if (this.symmetry) { + this.secondaryMovement.pop(); + } this.target = current; this.targetMaxProgress = null; this.progress = MAX_PROGRESS; @@ -334,20 +358,17 @@ Snake.prototype.getHead = function() { } return {x: x, y: y}; } -Snake.prototype.render = function() { - if (!this.anythingChanged()) { - return; - } +Snake.prototype.getRenderContents = function(movement, target) { // Initial output: start, direction. If last one, also include progress. // In the future, add arcs. var contents = []; var previous = null; - for (var i = 0; i <= this.movement.length; i++) { - var isEnd = i == this.movement.length; - if (isEnd && !this.target) { + for (var i = 0; i <= movement.length; i++) { + var isEnd = i == movement.length; + if (isEnd && !target) { continue; } - var coords = isEnd ? this.target : this.movement[i]; + var coords = isEnd ? target : movement[i]; var segment = {i: coords.i, j: coords.j}; if (!previous) { segment.segmentType = SegmentType.START; @@ -367,17 +388,36 @@ Snake.prototype.render = function() { contents.push(segment); previous = coords; } + return contents; +} +Snake.prototype.renderSingle = function(contents, snakeId, snakeEl) { // Ugly DOM manipulation to insert SVG dynamically. - if (!this.snakeEl) { - this.snakeEl = document.createElementNS("http://www.w3.org/2000/svg", 'g') - this.snakeEl.setAttribute('id', 'path' + this.snakeId); - this.draw.appendChild(this.snakeEl); + if (!snakeEl) { + snakeEl = document.createElementNS('http://www.w3.org/2000/svg', 'g') + snakeEl.setAttribute('id', 'path' + snakeId); + this.draw.appendChild(snakeEl); } // TODO: Incremental update to make performance even better. goog.soy.renderElement( - this.snakeEl, + snakeEl, windmill.templates.snakeSvg, {contents: contents}); + return snakeEl; +} +Snake.prototype.render = function() { + if (!this.anythingChanged()) { + return; + } + var contents = this.getRenderContents(this.movement, this.target); + this.snakeEl = this.renderSingle(contents, this.snakeId, this.snakeEl); + if (this.symmetry) { + var symTarget = this.target ? this.symmetry.reflectPoint(this.target) : null; + contents = this.getRenderContents(this.secondaryMovement, symTarget); + this.secondarySnakeEl = this.renderSingle( + contents, + this.secondarySnakeId, + this.secondarySnakeEl); + } } Snake.prototype.anythingChanged = function() { // Cute little hash code avoid constantly rendering. @@ -404,23 +444,28 @@ Snake.prototype.stringRepr = function() { } return record.join(' '); } -Snake.prototype.fade = function(opt_timeout, opt_callback) { - if (!this.snakeEl) { - throw Error(); - } - // Optimized for single-snake case. +Snake.prototype.fadeSingle = function(snakeEl, opt_timeout, opt_callback) { if (opt_timeout) { - this.snakeEl.style.transition = 'opacity ' + opt_timeout + 'ms ease-out'; - this.snakeEl.style.opacity = '0'; + snakeEl.style.transition = 'opacity ' + opt_timeout + 'ms ease-out'; + snakeEl.style.opacity = '0'; setTimeout(goog.bind(function() { - this.snakeEl.innerHTML = ''; - this.snakeEl.parentNode.removeChild(this.snakeEl); + snakeEl.innerHTML = ''; + snakeEl.parentNode.removeChild(snakeEl); if (opt_callback) { opt_callback(); } }, this), opt_timeout); } else { - this.snakeEl.parentNode.removeChild(this.snakeEl); + snakeEl.parentNode.removeChild(snakeEl); + } +} +Snake.prototype.fade = function(opt_timeout, opt_callback) { + if (!this.snakeEl) { + throw Error(); + } + this.fadeSingle(this.snakeEl, opt_timeout, opt_callback); + if (this.secondarySnakeEl) { + this.fadeSingle(this.secondarySnakeEl, opt_timeout); } } diff --git a/src/ui.js b/src/ui.js index c4b0abc..7f17f59 100644 --- a/src/ui.js +++ b/src/ui.js @@ -243,10 +243,14 @@ GridUi.prototype.clearEditEntity = function() { } GridUi.prototype.setEditEntity = function(data) { // This method *always* renders anew. + // TODO: Should move setting width/height/symmetry to a different method? this.editEntity = null; if (data.width != this.grid.width || data.height != this.grid.height) { this.grid.initialize(data.width, data.height); } + if (data.symmetry != this.grid.symmetry) { + this.grid.setSymmetry(data.symmetry); + } var entityMap = { 'basic': Type.BASIC, 'start': Type.START, @@ -311,24 +315,49 @@ GridUi.prototype.attemptInsert = function(coord, drawType, el) { if (!this.editEntity) { return; } + var currentVal = this.grid.drawTypeEntity(coord, drawType); + var otherCoord = goog.bind(function(value) { + // TODO: Should probably return a new DrawType when START/END + // vlines/hlines exist. + var sym = this.grid.getSymmetry(); + if (sym && (value.type == Type.START || value.type == Type.END || + currentVal.type == Type.START || currentVal.type == Type.END)) { + return sym.reflectPoint(coord); + } + }, this); if (el.getAttribute('data-op') == '1') { + var insert = goog.bind(function(coord, drawType) { + var entity = new Entity(this.editEntity); + if (entity.type == Type.END) { + var orient = this.grid.getEndPlacement(coord.i, coord.j); + if (!orient) { + return; + } + entity.orientation = orient; + } + this.grid.drawTypeEntity(coord, drawType, entity); + }, this); // Regular insertion - var entity = new Entity(this.editEntity); - if (entity.type == Type.END) { - var orient = this.grid.getEndPlacement(coord.i, coord.j); - if (!orient) { - return; + var other = otherCoord(this.editEntity); + insert(coord, drawType); + if (other) { + if (this.editEntity.type == Type.START || this.editEntity.type == Type.END) { + insert(other, drawType); + } else { + this.grid.drawTypeEntity(other, drawType, new Entity()); } - entity.orientation = orient; } this.puzzleVersion++; - this.grid.drawTypeEntity(coord, drawType, entity); el.setAttribute('data-op', 2); goog.dom.classlist.add(el, 'isRemoval'); } else { // Removal - this.puzzleVersion++; + var other = otherCoord(this.grid.drawTypeEntity(coord, drawType)); this.grid.drawTypeEntity(coord, drawType, new Entity()); + if (other) { + this.grid.drawTypeEntity(other, drawType, new Entity()); + } + this.puzzleVersion++; if (this.editEntity.type != Type.BASIC) { el.setAttribute('data-op', 1); goog.dom.classlist.remove(el, 'isRemoval'); @@ -407,7 +436,10 @@ GridUi.prototype.onPointerLock = function(turnt) { GridUi.prototype.initializeSnakeInternal = function( coords, opt_mouseCoords, opt_isTouch) { this.snake = new Snake( - coords, document.getElementById('gridPath'), opt_mouseCoords); + coords, + document.getElementById('gridPath'), + opt_mouseCoords, + this.grid.getSymmetry() || undefined); this.snakeHandler = new goog.events.EventHandler(this); var ignoreNextClick = false; // TODO: This doesn't actually seem to work anymore. Why?? @@ -498,7 +530,6 @@ GridUi.prototype.updateSnake = function() { }, this), 0); } - GridUi.prototype.finishSnake = function() { if (!this.snake) { return; @@ -536,7 +567,7 @@ GridUi.prototype.finishSnake = function() { return; } // Success or failure at end - var errs = windmill.validate.getErrors(this.grid, this.snake.movement); + var errs = windmill.validate.getErrors(this.grid, this.snake.movement, this.snake.secondaryMovement); if (errs.messages.length) { var messages = []; goog.array.removeDuplicates(errs.messages, messages); @@ -659,7 +690,7 @@ GridUi.NavigationSelector.prototype.pointIsReachable = function( return 'no'; } GridUi.NavigationSelector.prototype.selectTarget = function( - movement, di, dj, preferHorizontal) { + di, dj, preferHorizontal, movement, secondaryMovement) { var grid = this.grid; // Select something var current = movement[movement.length - 1]; @@ -673,20 +704,53 @@ GridUi.NavigationSelector.prototype.selectTarget = function( diBack = previous.i - current.i; djBack = previous.j - current.j; } + var secondary = null; + if (secondaryMovement) { + var symmetry = grid.getSymmetry(); + secondary = { + symmetry: symmetry, + movement: secondaryMovement, + current: secondaryMovement[secondaryMovement.length - 1] + }; + } var crossesPath = function(di, dj) { - return goog.array.find(movement, function(coord) { + var blocker = goog.array.find(movement, function(coord) { var targetIsCoord = coord.i == current.i + di && coord.j == current.j + dj; var isBacktrack = di == diBack && dj == djBack; return targetIsCoord ? !isBacktrack : false; }); + if (blocker != null) { + return {blocker: blocker, midway: false}; + } + if (secondary) { + var sd = symmetry.reflectDelta({di: di, dj: dj}); + if (current.i + di == secondary.current.i + sd.di && + current.j + dj == secondary.current.j + sd.dj) { + return {vertex: true, midway: true}; + } + if (current.i + di == secondary.current.i && + current.j + dj == secondary.current.j) { + return {vertex: false, midway: true}; + } + blocker = goog.array.find(secondary.movement, function(coord) { + var targetIsCoord = + coord.i == current.i + di && coord.j == current.j + dj; + return targetIsCoord; + }); + if (blocker != null) { + return {blocker: blocker, midway: false}; + } + } + return null; } var calcProgress = function(di, dj) { if (di == 0 && dj == 0) { return 'no'; } + // TODO: Clean this up, so length is calculated at the very + // end and semantic meaning is preserved (e.g. for isEnd). var reach = this.pointIsReachable(current, di, dj); - var blocker = crossesPath(di, dj); if (reach == 'no') { return 0; } else if (reach == 'end') { @@ -694,12 +758,30 @@ GridUi.NavigationSelector.prototype.selectTarget = function( } else if (reach == 'disjoint') { return UI.DISJOINT_LENGTH; } - if (blocker) { - var point = grid.pointEntity(blocker.i, blocker.j); - if (!point) { - throw Error('bad element in path'); + if (secondary) { + var sd = symmetry.reflectDelta({di: di, dj: dj}); + reach = this.pointIsReachable( + secondary.current, sd.di, sd.dj); + if (reach == 'no') { + return 0; + } else if (reach == 'end') { + return UI.END_LENGTH; + } else if (reach == 'disjoint') { + return UI.DISJOINT_LENGTH; + } + } + var cross = crossesPath(di, dj); + if (cross) { + var blocker = cross.blocker; + if (blocker) { + var point = grid.pointEntity(blocker.i, blocker.j); + if (!point) { + throw Error('bad element in path'); + } + return point.type == Type.START ? 65 : 80; + } else if (cross.midway) { + return cross.vertex ? 90 : 40; } - return point.type == Type.START ? 50 : 70; } return -1; } diff --git a/src/validate.js b/src/validate.js index 7d1de85..32db7db 100644 --- a/src/validate.js +++ b/src/validate.js @@ -30,8 +30,19 @@ var shapeKey = windmill.keys.shapeKey; windmill.validate = {}; -windmill.validate.getErrors = function(grid, movement) { - var p = new windmill.Path(movement, grid.width, grid.height); +windmill.validate.getErrors = function(grid, movement, secondaryMovement) { + var p; + if (secondaryMovement) { + p = new windmill.Path( + goog.array.concat(movement, secondaryMovement), + grid.width, + grid.height, + [movement.length]); + } else { + p = new windmill.Path( + movement, grid.width, + grid.height); + } var originalErrors = []; // Per-entity checks grid.forEachEntity(function(value, i, j, drawType) { @@ -146,6 +157,8 @@ windmill.validate.getErrors = function(grid, movement) { color = val.cell.shape.negative ? Color.BLUE : Color.YELLOW; } else if (val.cell.type == Type.ERROR) { color = Color.WHITE; + } else if (val.cell.type == Type.TRIANGLE) { + color = Color.ORANGE; } else { return; } diff --git a/src/windmill.soy b/src/windmill.soy index 3757d87..8775e58 100644 --- a/src/windmill.soy +++ b/src/windmill.soy @@ -38,6 +38,8 @@ green {case Color.BLUE} blue + {case Color.ORANGE} + orange {/switch} {else} gray @@ -292,7 +294,7 @@ //{let $coords: ['x': 10, 'y': 10] /}{let $scale: 0.8 /} {foreach $color in [Color.BLACK, Color.WHITE, Color.CYAN, Color.MAGENTA, Color.YELLOW, - Color.RED, Color.GREEN, Color.BLUE]} + Color.RED, Color.GREEN, Color.BLUE, Color.ORANGE]} {call .squareSvg data="$coords"} @@ -530,7 +532,7 @@ {@param extras: ?} {@param editEntity: ?} //{@param type: number} -{let $entity: ['x': UI.GRID_UNIT*$i, 'y': UI.GRID_UNIT*$j, 'i': $i, 'j': $j, 'drawType': $drawType, 'highlight': true, 'removeAlways': $editEntity.type == Type.BASIC] /} +{let $entity: ['x': UI.GRID_UNIT*$i, 'y': UI.GRID_UNIT*$j, 'i': $i, 'j': $j, 'drawType': $drawType, 'highlight': true, 'alwaysRemove': $editEntity.type == Type.BASIC] /} {let $remove: $type == $editEntity.type /} // Holy switch {switch $drawType} diff --git a/static/witness.svg b/static/witness.svg index 57355cc..7d37c33 100644 --- a/static/witness.svg +++ b/static/witness.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file