diff --git a/core/block_render_svg.js b/core/block_render_svg.js index 9edb3de8b..c4d503370 100644 --- a/core/block_render_svg.js +++ b/core/block_render_svg.js @@ -29,6 +29,7 @@ goog.provide('Blockly.BlockSvg.render'); goog.require('Blockly.BlockSvg'); + // UI constants for rendering blocks. /** * Horizontal space between elements. diff --git a/core/connection.js b/core/connection.js index 2fd2cb60e..44a213191 100644 --- a/core/connection.js +++ b/core/connection.js @@ -25,7 +25,6 @@ 'use strict'; goog.provide('Blockly.Connection'); -goog.provide('Blockly.ConnectionDB'); goog.require('goog.dom'); @@ -153,6 +152,18 @@ Blockly.Connection.prototype.isSuperior = function() { this.type == Blockly.NEXT_STATEMENT; }; +/** + * Returns the distance between this connection and another connection. + * @param {!Blockly.Connection} otherConnection The other connection to measure + * the distance to. + * @return {number} The distance between connections. + */ +Blockly.Connection.prototype.distanceFrom = function(otherConnection) { + var xDiff = this.x_ - otherConnection.x_; + var yDiff = this.y_ - otherConnection.y_; + return Math.sqrt(xDiff * xDiff + yDiff * yDiff); +}; + /** * Checks whether the current connection can connect with the target * connection. @@ -207,6 +218,60 @@ Blockly.Connection.prototype.checkConnection_ = function(target) { } }; +/** + * Check if the two connections can be dragged to connect to each other. + * @param {Blockly.Connection} candidate A nearby connection to check. + * @param {number} maxRadius The maximum radius allowed for connections. + * @return {boolean} True if the connection is allowed, false otherwise. + */ +Blockly.Connection.prototype.isConnectionAllowed = function(candidate, + maxRadius) { + if (this.distanceFrom(candidate) > maxRadius) { + return false; + } + + // Type checking + var canConnect = this.canConnectWithReason_(candidate); + if (canConnect != Blockly.Connection.CAN_CONNECT && + canConnect != Blockly.Connection.REASON_MUST_DISCONNECT) { + return false; + } + + // Don't offer to connect an already connected left (male) value plug to + // an available right (female) value plug. Don't offer to connect the + // bottom of a statement block to one that's already connected. + if (candidate.type == Blockly.OUTPUT_VALUE || + candidate.type == Blockly.PREVIOUS_STATEMENT) { + if (candidate.targetConnection || this.targetConnection) { + return false; + } + } + + // Offering to connect the left (male) of a value block to an already + // connected value pair is ok, we'll splice it in. + // However, don't offer to splice into an unmovable block. + if (candidate.type == Blockly.INPUT_VALUE && + candidate.targetConnection && + !candidate.targetBlock().isMovable() && + !candidate.targetBlock().isShadow()) { + return false; + } + + // Don't let blocks try to connect to themselves or ones they nest. + var targetSourceBlock = candidate.sourceBlock_; + var sourceBlock = this.sourceBlock_; + if (targetSourceBlock && sourceBlock) { + do { + if (sourceBlock == targetSourceBlock) { + return false; + } + targetSourceBlock = targetSourceBlock.getParent(); + } while (targetSourceBlock); + } + + return true; +}; + /** * Connect this connection to another connection. * @param {!Blockly.Connection} otherConnection Connection to connect to. @@ -332,7 +397,7 @@ Blockly.Connection.connectReciprocally = function(first, second) { } first.targetConnection = second; second.targetConnection = first; -} +}; /** * Does the given block have one and only one connection point that will accept @@ -503,7 +568,7 @@ Blockly.Connection.prototype.moveTo = function(x, y) { this.y_ = y; // Insert it into its new location in the database. if (!this.hidden_) { - this.db_.addConnection_(this); + this.db_.addConnection(this); } }; @@ -516,38 +581,6 @@ Blockly.Connection.prototype.moveBy = function(dx, dy) { this.moveTo(this.x_ + dx, this.y_ + dy); }; -/** - * Add highlighting around this connection. - */ -Blockly.Connection.prototype.highlight = function() { - var steps; - if (this.type == Blockly.INPUT_VALUE || this.type == Blockly.OUTPUT_VALUE) { - var tabWidth = this.sourceBlock_.RTL ? -Blockly.BlockSvg.TAB_WIDTH : - Blockly.BlockSvg.TAB_WIDTH; - steps = 'm 0,0 ' + Blockly.BlockSvg.TAB_PATH_DOWN + ' v 5'; - - } else { - steps = 'm -20,0 h 5 ' + Blockly.BlockSvg.NOTCH_PATH_LEFT + ' h 5'; - } - var xy = this.sourceBlock_.getRelativeToSurfaceXY(); - var x = this.x_ - xy.x; - var y = this.y_ - xy.y; - Blockly.Connection.highlightedPath_ = Blockly.createSvgElement('path', - {'class': 'blocklyHighlightedConnectionPath', - 'd': steps, - transform: 'translate(' + x + ',' + y + ')' + - (this.sourceBlock_.RTL ? ' scale(-1 1)' : '')}, - this.sourceBlock_.getSvgRoot()); -}; - -/** - * Remove the highlighting around this connection. - */ -Blockly.Connection.prototype.unhighlight = function() { - goog.dom.removeNode(Blockly.Connection.highlightedPath_); - delete Blockly.Connection.highlightedPath_; -}; - /** * Move the blocks on either side of this connection right next to each other. * @private @@ -575,108 +608,18 @@ Blockly.Connection.prototype.tighten_ = function() { * in the database and the current location (as a result of dragging). * @param {number} dy Vertical offset between this connection's location * in the database and the current location (as a result of dragging). - * @return {!{connection: ?Blockly.Connection, radius: number}} Contains two properties: 'connection' which is either - * another connection or null, and 'radius' which is the distance. + * @return {!{connection: ?Blockly.Connection, radius: number}} Contains two + * properties:' connection' which is either another connection or null, + * and 'radius' which is the distance. */ Blockly.Connection.prototype.closest = function(maxLimit, dx, dy) { - if (this.targetConnection) { - // Don't offer to connect to a connection that's already connected. - return {connection: null, radius: maxLimit}; + var closestConnection = this.dbOpposite_.searchForClosest(this, maxLimit, dx, + dy); + if (closestConnection) { + return {connection: closestConnection, + radius: this.distanceFrom(closestConnection)}; } - // Determine the opposite type of connection. - var db = this.dbOpposite_; - - // Since this connection is probably being dragged, add the delta. - var currentX = this.x_ + dx; - var currentY = this.y_ + dy; - - // Binary search to find the closest y location. - var pointerMin = 0; - var pointerMax = db.length - 2; - var pointerMid = pointerMax; - while (pointerMin < pointerMid) { - if (db[pointerMid].y_ < currentY) { - pointerMin = pointerMid; - } else { - pointerMax = pointerMid; - } - pointerMid = Math.floor((pointerMin + pointerMax) / 2); - } - - // Walk forward and back on the y axis looking for the closest x,y point. - pointerMin = pointerMid; - pointerMax = pointerMid; - var closestConnection = null; - var sourceBlock = this.sourceBlock_; - var thisConnection = this; - if (db.length) { - while (pointerMin >= 0 && checkConnection_(pointerMin)) { - pointerMin--; - } - do { - pointerMax++; - } while (pointerMax < db.length && checkConnection_(pointerMax)); - } - - /** - * Computes if the current connection is within the allowed radius of another - * connection. - * This function is a closure and has access to outside variables. - * @param {number} yIndex The other connection's index in the database. - * @return {boolean} True if the search needs to continue: either the current - * connection's vertical distance from the other connection is less than - * the allowed radius, or if the connection is not compatible. - * @private - */ - function checkConnection_(yIndex) { - var connection = db[yIndex]; - if (connection.type == Blockly.OUTPUT_VALUE || - connection.type == Blockly.PREVIOUS_STATEMENT) { - // Don't offer to connect an already connected left (male) value plug to - // an available right (female) value plug. Don't offer to connect the - // bottom of a statement block to one that's already connected. - if (connection.targetConnection) { - return true; - } - } - // Offering to connect the top of a statement block to an already connected - // connection is ok, we'll just insert it into the stack. - - // Offering to connect the left (male) of a value block to an already - // connected value pair is ok, we'll splice it in. - // However, don't offer to splice into an unmovable block. - if (connection.type == Blockly.INPUT_VALUE && - connection.targetConnection && - !connection.targetBlock().isMovable() && - !connection.targetBlock().isShadow()) { - return true; - } - - // Do type checking. - if (!thisConnection.checkType_(connection)) { - return true; - } - - // Don't let blocks try to connect to themselves or ones they nest. - var targetSourceBlock = connection.sourceBlock_; - do { - if (sourceBlock == targetSourceBlock) { - return true; - } - targetSourceBlock = targetSourceBlock.getParent(); - } while (targetSourceBlock); - - // Only connections within the maxLimit radius. - var dx = currentX - connection.x_; - var dy = currentY - connection.y_; - var r = Math.sqrt(dx * dx + dy * dy); - if (r <= maxLimit) { - closestConnection = connection; - maxLimit = r; - } - return Math.abs(dy) < maxLimit; - } - return {connection: closestConnection, radius: maxLimit}; + return {connection: null, radius: maxLimit}; }; /** @@ -763,59 +706,11 @@ Blockly.Connection.prototype.getShadowDom = function() { * @private */ Blockly.Connection.prototype.neighbours_ = function(maxLimit) { - // Determine the opposite type of connection. - var db = this.dbOpposite_; - - var currentX = this.x_; - var currentY = this.y_; - - // Binary search to find the closest y location. - var pointerMin = 0; - var pointerMax = db.length - 2; - var pointerMid = pointerMax; - while (pointerMin < pointerMid) { - if (db[pointerMid].y_ < currentY) { - pointerMin = pointerMid; - } else { - pointerMax = pointerMid; - } - pointerMid = Math.floor((pointerMin + pointerMax) / 2); - } - - // Walk forward and back on the y axis looking for the closest x,y point. - pointerMin = pointerMid; - pointerMax = pointerMid; - var neighbours = []; - var sourceBlock = this.sourceBlock_; - if (db.length) { - while (pointerMin >= 0 && checkConnection_(pointerMin)) { - pointerMin--; - } - do { - pointerMax++; - } while (pointerMax < db.length && checkConnection_(pointerMax)); - } - - /** - * Computes if the current connection is within the allowed radius of another - * connection. - * This function is a closure and has access to outside variables. - * @param {number} yIndex The other connection's index in the database. - * @return {boolean} True if the current connection's vertical distance from - * the other connection is less than the allowed radius. - */ - function checkConnection_(yIndex) { - var dx = currentX - db[yIndex].x_; - var dy = currentY - db[yIndex].y_; - var r = Math.sqrt(dx * dx + dy * dy); - if (r <= maxLimit) { - neighbours.push(db[yIndex]); - } - return dy < maxLimit; - } - return neighbours; + return this.dbOpposite_.getNeighbours(this, maxLimit); }; +// Appearance or lack thereof. + /** * Set whether this connections is hidden (not tracked in a database) or not. * @param {boolean} hidden True if connection is hidden. @@ -825,7 +720,7 @@ Blockly.Connection.prototype.setHidden = function(hidden) { if (hidden && this.inDB_) { this.db_.removeConnection_(this); } else if (!hidden && !this.inDB_) { - this.db_.addConnection_(this); + this.db_.addConnection(this); } }; @@ -895,110 +790,34 @@ Blockly.Connection.prototype.unhideAll = function() { return renderList; }; - /** - * Database of connections. - * Connections are stored in order of their vertical component. This way - * connections in an area may be looked up quickly using a binary search. - * @constructor + * Add highlighting around this connection. */ -Blockly.ConnectionDB = function() { -}; +Blockly.Connection.prototype.highlight = function() { + var steps; + if (this.type == Blockly.INPUT_VALUE || this.type == Blockly.OUTPUT_VALUE) { + var tabWidth = this.sourceBlock_.RTL ? -Blockly.BlockSvg.TAB_WIDTH : + Blockly.BlockSvg.TAB_WIDTH; + steps = 'm 0,0 ' + Blockly.BlockSvg.TAB_PATH_DOWN + ' v 5'; -Blockly.ConnectionDB.prototype = new Array(); -/** - * Don't inherit the constructor from Array. - * @type {!Function} - */ -Blockly.ConnectionDB.constructor = Blockly.ConnectionDB; - -/** - * Add a connection to the database. Must not already exist in DB. - * @param {!Blockly.Connection} connection The connection to be added. - * @private - */ -Blockly.ConnectionDB.prototype.addConnection_ = function(connection) { - if (connection.inDB_) { - throw 'Connection already in database.'; + } else { + steps = 'm -20,0 h 5 ' + Blockly.BlockSvg.NOTCH_PATH_LEFT + ' h 5'; } - if (connection.sourceBlock_.isInFlyout) { - // Don't bother maintaining a database of connections in a flyout. - return; - } - // Insert connection using binary search. - var pointerMin = 0; - var pointerMax = this.length; - while (pointerMin < pointerMax) { - var pointerMid = Math.floor((pointerMin + pointerMax) / 2); - if (this[pointerMid].y_ < connection.y_) { - pointerMin = pointerMid + 1; - } else if (this[pointerMid].y_ > connection.y_) { - pointerMax = pointerMid; - } else { - pointerMin = pointerMid; - break; - } - } - this.splice(pointerMin, 0, connection); - connection.inDB_ = true; + var xy = this.sourceBlock_.getRelativeToSurfaceXY(); + var x = this.x_ - xy.x; + var y = this.y_ - xy.y; + Blockly.Connection.highlightedPath_ = Blockly.createSvgElement('path', + {'class': 'blocklyHighlightedConnectionPath', + 'd': steps, + transform: 'translate(' + x + ',' + y + ')' + + (this.sourceBlock_.RTL ? ' scale(-1 1)' : '')}, + this.sourceBlock_.getSvgRoot()); }; /** - * Remove a connection from the database. Must already exist in DB. - * @param {!Blockly.Connection} connection The connection to be removed. - * @private + * Remove the highlighting around this connection. */ -Blockly.ConnectionDB.prototype.removeConnection_ = function(connection) { - if (!connection.inDB_) { - throw 'Connection not in database.'; - } - connection.inDB_ = false; - // Find the connection using a binary search. - // About 10% faster than a linear search using indexOf. - var pointerMin = 0; - var pointerMax = this.length - 2; - var pointerMid = pointerMax; - while (pointerMin < pointerMid) { - if (this[pointerMid].y_ < connection.y_) { - pointerMin = pointerMid; - } else { - pointerMax = pointerMid; - } - pointerMid = Math.floor((pointerMin + pointerMax) / 2); - } - - // Walk forward and back on the y axis looking for the connection. - // When found, splice it out of the array. - pointerMin = pointerMid; - pointerMax = pointerMid; - while (pointerMin >= 0 && this[pointerMin].y_ == connection.y_) { - if (this[pointerMin] == connection) { - this.splice(pointerMin, 1); - return; - } - pointerMin--; - } - do { - if (this[pointerMax] == connection) { - this.splice(pointerMax, 1); - return; - } - pointerMax++; - } while (pointerMax < this.length && - this[pointerMax].y_ == connection.y_); - throw 'Unable to find connection in connectionDB.'; -}; - -/** - * Initialize a set of connection DBs for a specified workspace. - * @param {!Blockly.Workspace} workspace The workspace this DB is for. - */ -Blockly.ConnectionDB.init = function(workspace) { - // Create four databases, one for each connection type. - var dbList = []; - dbList[Blockly.INPUT_VALUE] = new Blockly.ConnectionDB(); - dbList[Blockly.OUTPUT_VALUE] = new Blockly.ConnectionDB(); - dbList[Blockly.NEXT_STATEMENT] = new Blockly.ConnectionDB(); - dbList[Blockly.PREVIOUS_STATEMENT] = new Blockly.ConnectionDB(); - workspace.connectionDBList = dbList; +Blockly.Connection.prototype.unhighlight = function() { + goog.dom.removeNode(Blockly.Connection.highlightedPath_); + delete Blockly.Connection.highlightedPath_; }; diff --git a/core/connection_db.js b/core/connection_db.js new file mode 100644 index 000000000..b64c5e23a --- /dev/null +++ b/core/connection_db.js @@ -0,0 +1,291 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Components for managing connections between blocks. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.ConnectionDB'); + +goog.require('Blockly.Connection'); + + +/** + * Database of connections. + * Connections are stored in order of their vertical component. This way + * connections in an area may be looked up quickly using a binary search. + * @constructor + */ +Blockly.ConnectionDB = function() { +}; + +Blockly.ConnectionDB.prototype = new Array(); +/** + * Don't inherit the constructor from Array. + * @type {!Function} + */ +Blockly.ConnectionDB.constructor = Blockly.ConnectionDB; + +/** + * Add a connection to the database. Must not already exist in DB. + * @param {!Blockly.Connection} connection The connection to be added. + * @private + */ +Blockly.ConnectionDB.prototype.addConnection = function(connection) { + if (connection.inDB_) { + throw 'Connection already in database.'; + } + if (connection.sourceBlock_.isInFlyout) { + // Don't bother maintaining a database of connections in a flyout. + return; + } + var position = this.findPositionForConnection_(connection); + this.splice(position, 0, connection); + connection.inDB_ = true; +}; + +/** + * Find the given connection. + * Starts by doing a binary search to find the approximate location, then + * linearly searches nearby for the exact connection. + * @param {Blockly.Connection} conn The connection to find. + * @return The index of the connection, or -1 if the connection was not found. + */ +Blockly.ConnectionDB.prototype.findConnection = function(conn) { + if (this.length == 0) { + return -1; + } + + var bestGuess = this.findPositionForConnection_(conn); + if (bestGuess >= this.length) { + // Not in list + return -1; + } + + var yPos = conn.y_; + // Walk forward and back on the y axis looking for the connection. + var pointerMin = bestGuess; + var pointerMax = bestGuess; + while(pointerMin >= 0 && this[pointerMin].y_ == yPos) { + if (this[pointerMin] == conn) { + return pointerMin; + } + pointerMin--; + } + + while (pointerMax < this.length && this[pointerMax].y_ == yPos) { + if (this[pointerMax] == conn) { + return pointerMax; + } + pointerMax++; + } + return -1; +}; + +/** + * Finds a candidate position for inserting this connection into the list. + * This will be in the correct y order but makes no guarantees about ordering in + * the x axis. + * @param {Blockly.Connection} connection The connection to insert. + * @return {number} The candidate index. + * @private + */ +Blockly.ConnectionDB.prototype.findPositionForConnection_ = function(connection) { + if (this.length == 0) { + return 0; + } + var pointerMin = 0; + var pointerMax = this.length; + while (pointerMin < pointerMax) { + var pointerMid = Math.floor((pointerMin + pointerMax) / 2); + if (this[pointerMid].y_ < connection.y_) { + pointerMin = pointerMid + 1; + } else if (this[pointerMid].y_ > connection.y_) { + pointerMax = pointerMid; + } else { + pointerMin = pointerMid; + break; + } + } + return pointerMin; +}; + +/** + * Remove a connection from the database. Must already exist in DB. + * @param {!Blockly.Connection} connection The connection to be removed. + * @private + */ +Blockly.ConnectionDB.prototype.removeConnection_ = function(connection) { + if (!connection.inDB_) { + throw 'Connection not in database.'; + } + var removalIndex = this.findConnection(connection); + if (removalIndex == -1) { + throw 'Unable to find connection in connectionDB.'; + } + connection.inDB_ = false; + this.splice(removalIndex, 1); +}; + +/** + * Find all nearby connections to the given connection. + * Type checking does not apply, since this function is used for bumping. + * @param {!Blockly.Connection} connection The connection whose neighbours should + * be returned. + * @param {number} maxRadius The maximum radius to another connection. + * @return {!Array.} List of connections. + * @private + */ +Blockly.ConnectionDB.prototype.getNeighbours = function(connection, maxRadius) { + var db = this; + var currentX = connection.x_; + var currentY = connection.y_; + + // Binary search to find the closest y location. + var pointerMin = 0; + var pointerMax = db.length - 2; + var pointerMid = pointerMax; + while (pointerMin < pointerMid) { + if (db[pointerMid].y_ < currentY) { + pointerMin = pointerMid; + } else { + pointerMax = pointerMid; + } + pointerMid = Math.floor((pointerMin + pointerMax) / 2); + } + + // Walk forward and back on the y axis looking for the closest x,y point. + pointerMin = pointerMid; + pointerMax = pointerMid; + var neighbours = []; + var sourceBlock = connection.sourceBlock_; + if (db.length) { + while (pointerMin >= 0 && checkConnection_(pointerMin)) { + pointerMin--; + } + do { + pointerMax++; + } while (pointerMax < db.length && checkConnection_(pointerMax)); + } + + /** + * Computes if the current connection is within the allowed radius of another + * connection. + * This function is a closure and has access to outside variables. + * @param {number} yIndex The other connection's index in the database. + * @return {boolean} True if the current connection's vertical distance from + * the other connection is less than the allowed radius. + */ + function checkConnection_(yIndex) { + var dx = currentX - db[yIndex].x_; + var dy = currentY - db[yIndex].y_; + var r = Math.sqrt(dx * dx + dy * dy); + if (r <= maxRadius) { + neighbours.push(db[yIndex]); + } + return dy < maxRadius; + } + return neighbours; +}; + + +Blockly.ConnectionDB.prototype.isInYRange_ = function(index, baseY, maxRadius) { + return (Math.abs(this[index].y_ - baseY) <= maxRadius); +} + +/** + * Find the closest compatible connection to this connection. + * @param {Blockly.Connection} conn The connection searching for a compatible + * mate. + * @param {number} maxRadius The maximum radius to another connection. + * @param {number} dx Horizontal offset between this connection's location + * in the database and the current location (as a result of dragging). + * @param {number} dy Vertical offset between this connection's location + * in the database and the current location (as a result of dragging). + * @return ?Blockly.Connection the closest valid connection. + * another connection or null, and 'radius' which is the distance. + */ +Blockly.ConnectionDB.prototype.searchForClosest = function(conn, maxRadius, dx, + dy) { + // Don't bother. + if (!this.length) { + return null; + } + + // Stash the values of x and y from before the drag. + var baseY = conn.y_; + var baseX = conn.x_; + + conn.x_ = baseX + dx; + conn.y_ = baseY + dy; + + // findPositionForConnection finds an index for insertion, which is always + // after any block with the same y index. We want to search both forward + // and back, so search on both sides of the index. + var closestIndex = this.findPositionForConnection_(conn); + + var bestConnection = null; + var bestRadius = maxRadius; + var temp; + + // Walk forward and back on the y axis looking for the closest x,y point. + var pointerMin = closestIndex - 1; + while (pointerMin >= 0 && + this.isInYRange_(pointerMin, conn.y_, maxRadius)) { + temp = this[pointerMin]; + if (conn.isConnectionAllowed(temp, bestRadius)) { + bestConnection = temp; + bestRadius = temp.distanceFrom(conn); + } + pointerMin--; + } + + var pointerMax = closestIndex; + while (pointerMax < this.length && this.isInYRange_(pointerMax, conn.y_, + maxRadius)) { + temp = this[pointerMax]; + if (conn.isConnectionAllowed(temp, bestRadius)) { + bestConnection = temp; + bestRadius = temp.distanceFrom(conn); + } + pointerMax++; + } + + // Reset the values of x and y. + conn.x_ = baseX; + conn.y_ = baseY; + return bestConnection; +}; + +/** + * Initialize a set of connection DBs for a specified workspace. + * @param {!Blockly.Workspace} workspace The workspace this DB is for. + */ +Blockly.ConnectionDB.init = function(workspace) { + // Create four databases, one for each connection type. + var dbList = []; + dbList[Blockly.INPUT_VALUE] = new Blockly.ConnectionDB(); + dbList[Blockly.OUTPUT_VALUE] = new Blockly.ConnectionDB(); + dbList[Blockly.NEXT_STATEMENT] = new Blockly.ConnectionDB(); + dbList[Blockly.PREVIOUS_STATEMENT] = new Blockly.ConnectionDB(); + workspace.connectionDBList = dbList; +}; diff --git a/tests/jsunit/connection_db_test.js b/tests/jsunit/connection_db_test.js index c7f897168..0e7b33401 100644 --- a/tests/jsunit/connection_db_test.js +++ b/tests/jsunit/connection_db_test.js @@ -39,23 +39,23 @@ function verify_DB_(msg, expected, db) { function test_DB_addConnection() { var db = new Blockly.ConnectionDB(); var o2 = {y_: 2, sourceBlock_: {}}; - db.addConnection_(o2); + db.addConnection(o2); verify_DB_('Adding connection #2', [o2], db); var o4 = {y_: 4, sourceBlock_: {}}; - db.addConnection_(o4); + db.addConnection(o4); verify_DB_('Adding connection #4', [o2, o4], db); var o1 = {y_: 1, sourceBlock_: {}}; - db.addConnection_(o1); + db.addConnection(o1); verify_DB_('Adding connection #1', [o1, o2, o4], db); var o3a = {y_: 3, sourceBlock_: {}}; - db.addConnection_(o3a); + db.addConnection(o3a); verify_DB_('Adding connection #3a', [o1, o2, o3a, o4], db); var o3b = {y_: 3, sourceBlock_: {}}; - db.addConnection_(o3b); + db.addConnection(o3b); verify_DB_('Adding connection #3b', [o1, o2, o3b, o3a, o4], db); } @@ -67,12 +67,12 @@ function test_DB_removeConnection() { var o3b = {y_: 3, sourceBlock_: {}}; var o3c = {y_: 3, sourceBlock_: {}}; var o4 = {y_: 4, sourceBlock_: {}}; - db.addConnection_(o1); - db.addConnection_(o2); - db.addConnection_(o3c); - db.addConnection_(o3b); - db.addConnection_(o3a); - db.addConnection_(o4); + db.addConnection(o1); + db.addConnection(o2); + db.addConnection(o3c); + db.addConnection(o3b); + db.addConnection(o3a); + db.addConnection(o4); verify_DB_('Adding connections 1-4', [o1, o2, o3a, o3b, o3c, o4], db); db.removeConnection_(o2); @@ -93,3 +93,190 @@ function test_DB_removeConnection() { db.removeConnection_(o3b); verify_DB_('Removing connection #3b', [], db); } + +function test_DB_getNeighbours() { + var db = new Blockly.ConnectionDB(); + + // Search an empty list. + assertEquals(helper_getNeighbours(db, + 10 /* x */, 10 /* y */, 100 /* radius */).length, 0); + + // Set up some connections. + for (var i = 0; i < 10; i++) { + db.addConnection(helper_createConnection(0, i, + Blockly.PREVIOUS_STATEMENT)); + } + + // Test block belongs at beginning. + var result = helper_getNeighbours(db, 0, 0, 4); + assertEquals(5, result.length); + for (i = 0; i < result.length; i++) { + assertNotEquals(result.indexOf(db[i]), -1); // contains + } + + // Test block belongs at middle. + result = helper_getNeighbours(db, 0, 4, 2); + assertEquals(5, result.length); + for (i = 0; i < result.length; i++) { + assertNotEquals(result.indexOf(db[i + 2]), -1); // contains + } + + // Test block belongs at end. + result = helper_getNeighbours(db, 0, 9, 4); + assertEquals(5, result.length); + for (i = 0; i < result.length; i++) { + assertNotEquals(result.indexOf(db[i + 5]), -1); // contains + } + + // Test block has no neighbours due to being out of range in the x direction. + result = helper_getNeighbours(db, 10, 9, 4); + assertEquals(result.length, 0); + + // Test block has no neighbours due to being out of range in the y direction. + result = helper_getNeighbours(db, 0, 19, 4); + assertEquals(result.length, 0); + + // Test block has no neighbours due to being out of range diagonally. + result = helper_getNeighbours(db, -2, -2, 2); + assertEquals(result.length, 0); +} + +function test_DB_findPositionForConnection() { + var db = new Blockly.ConnectionDB(); + db.addConnection(helper_createConnection(0, 0, Blockly.PREVIOUS_STATEMENT)); + db.addConnection(helper_createConnection(0, 1, Blockly.PREVIOUS_STATEMENT)); + db.addConnection(helper_createConnection(0, 2, Blockly.PREVIOUS_STATEMENT)); + db.addConnection(helper_createConnection(0, 4, Blockly.PREVIOUS_STATEMENT)); + db.addConnection(helper_createConnection(0, 5, Blockly.PREVIOUS_STATEMENT)); + + assertEquals(5, db.length); + var conn = helper_createConnection(0, 3, Blockly.PREVIOUS_STATEMENT); + assertEquals(3, db.findPositionForConnection_(conn)); +} + +function test_DB_findConnection() { + var db = new Blockly.ConnectionDB(); + for (var i = 0; i < 10; i++) { + db.addConnection(helper_createConnection(i, 0, + Blockly.PREVIOUS_STATEMENT)); + db.addConnection(helper_createConnection(0, i, + Blockly.PREVIOUS_STATEMENT)); + } + + var conn = helper_createConnection(3, 3, Blockly.PREVIOUS_STATEMENT); + db.addConnection(conn); + assertEquals(conn, db[db.findConnection(conn)]); + + conn = helper_createConnection(3, 3, Blockly.PREVIOUS_STATEMENT); + assertEquals(-1, db.findConnection(conn)); +} + +function test_DB_ordering() { + var db = new Blockly.ConnectionDB(); + for (var i = 0; i < 10; i++) { + db.addConnection(helper_createConnection(0, 9 - i, + Blockly.PREVIOUS_STATEMENT)); + } + + for (i = 0; i < 10; i++) { + assertEquals(i, db[i].y_); + } + + // quasi-random + var xCoords = [-29, -47, -77, 2, 43, 34, -59, -52, -90, -36, -91, 38, 87, -20, + 60, 4, -57, 65, -37, -81, 57, 58, -96, 1, 67, -79, 34, 93, -90, -99, -62, + 4, 11, -36, -51, -72, 3, -50, -24, -45, -92, -38, 37, 24, -47, -73, 79, + -20, 99, 43, -10, -87, 19, 35, -62, -36, 49, 86, -24, -47, -89, 33, -44, + 25, -73, -91, 85, 6, 0, 89, -94, 36, -35, 84, -9, 96, -21, 52, 10, -95, 7, + -67, -70, 62, 9, -40, -95, -9, -94, 55, 57, -96, 55, 8, -48, -57, -87, 81, + 23, 65]; + var yCoords = [-81, 82, 5, 47, 30, 57, -12, 28, 38, 92, -25, -20, 23, -51, 73, + -90, 8, 28, -51, -15, 81, -60, -6, -16, 77, -62, -42, -24, 35, 95, -46, + -7, 61, -16, 14, 91, 57, -38, 27, -39, 92, 47, -98, 11, -33, -72, 64, 38, + -64, -88, -35, -59, -76, -94, 45, -25, -100, -95, 63, -97, 45, 98, 99, 34, + 27, 52, -18, -45, 66, -32, -38, 70, -73, -23, 5, -2, -13, -9, 48, 74, -97, + -11, 35, -79, -16, -77, 83, -57, -53, 35, -44, 100, -27, -15, 5, 39, 33, + -19, -20, -95]; + for (i = 0; i < xCoords.length; i++) { + db.addConnection(helper_createConnection(xCoords[i], yCoords[i], + Blockly.PREVIOUS_STATEMENT)); + } + + for (i = 1; i < xCoords.length; i++) { + assertTrue(db[i].y_ >= db[i - 1].y_); + } +} + +function test_SearchForClosest() { + var db = new Blockly.ConnectionDB(); + var sharedWorkspace = {id: "Shared workspace"}; + + // Search an empty list. + assertEquals(null, helper_searchDB(db, 10 /* x */, 10 /* y */, + 100 /* radius */)); + + db.addConnection(helper_createConnection(100, 0, Blockly.PREVIOUS_STATEMENT, + sharedWorkspace)); + assertEquals(null, helper_searchDB(db, 0, 0, 5, sharedWorkspace)); + + db = new Blockly.ConnectionDB(); + for (var i = 0; i < 10; i++) { + var tempConn = helper_createConnection(0, i, Blockly.PREVIOUS_STATEMENT, + sharedWorkspace); + tempConn.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace); + db.addConnection(tempConn); + } + + // Should be at 0, 9. + var last = db[db.length - 1]; + // Correct connection is last in db; many connections in radius. + assertEquals(last, helper_searchDB(db, 0, 10, 15, sharedWorkspace)); + // Nothing nearby. + assertEquals(null, helper_searchDB(db, 100, 100, 3, sharedWorkspace)); + // First in db, exact match. + assertEquals(db[0], helper_searchDB(db, 0, 0, 0, sharedWorkspace)); + + tempConn = helper_createConnection(6, 6, Blockly.PREVIOUS_STATEMENT, + sharedWorkspace); + tempConn.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace); + db.addConnection(tempConn); + tempConn = helper_createConnection(5, 5, Blockly.PREVIOUS_STATEMENT, + sharedWorkspace); + tempConn.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace); + db.addConnection(tempConn); + + var result = helper_searchDB(db, 4, 6, 3, sharedWorkspace); + assertEquals(5, result.x_); + assertEquals(5, result.y_); +} + + +function helper_getNeighbours(db, x, y, radius) { + return db.getNeighbours(helper_createConnection(x, y, Blockly.NEXT_STATEMENT), + radius); +} + +function helper_searchDB(db, x, y, radius, shared_workspace) { + var tempConn = helper_createConnection(x, y, + Blockly.NEXT_STATEMENT, shared_workspace); + tempConn.sourceBlock_ = helper_makeSourceBlock(shared_workspace); + return db.searchForClosest(tempConn, radius, 0, 0); +} + +function helper_makeSourceBlock(sharedWorkspace) { + return {workspace: sharedWorkspace, + parentBlock_: null, + getParent: function() { return null; }, + movable_: true, + isMovable: function() { return true; }, + isShadow: function() { return false; } + }; +} + +function helper_createConnection(x, y, type, opt_shared_workspace) { + var workspace = opt_shared_workspace ? opt_shared_workspace : {}; + var conn = new Blockly.Connection({workspace: workspace}, type); + conn.x_ = x; + conn.y_ = y; + return conn; +} \ No newline at end of file diff --git a/tests/jsunit/connection_test.js b/tests/jsunit/connection_test.js index b9bcc2b23..b4448b083 100644 --- a/tests/jsunit/connection_test.js +++ b/tests/jsunit/connection_test.js @@ -33,10 +33,14 @@ var dummyWorkspace; function connectionTest_setUp() { dummyWorkspace = {}; - input = new Blockly.Connection({workspace: dummyWorkspace}, Blockly.INPUT_VALUE); - output = new Blockly.Connection({workspace: dummyWorkspace}, Blockly.OUTPUT_VALUE); - previous = new Blockly.Connection({workspace: dummyWorkspace}, Blockly.PREVIOUS_STATEMENT); - next = new Blockly.Connection({workspace: dummyWorkspace}, Blockly.NEXT_STATEMENT); + input = new Blockly.Connection({workspace: dummyWorkspace}, + Blockly.INPUT_VALUE); + output = new Blockly.Connection({workspace: dummyWorkspace}, + Blockly.OUTPUT_VALUE); + previous = new Blockly.Connection({workspace: dummyWorkspace}, + Blockly.PREVIOUS_STATEMENT); + next = new Blockly.Connection({workspace: dummyWorkspace}, + Blockly.NEXT_STATEMENT); } function connectionTest_tearDown() { @@ -48,13 +52,14 @@ function connectionTest_tearDown() { } /** - * These tests check that the reasons for failures to connect are consistent (internal view of - * error states). + * These tests check that the reasons for failures to connect are consistent + * (internal view of error states). */ function testCanConnectWithReason_TargetNull() { connectionTest_setUp(); - assertEquals(Blockly.Connection.REASON_TARGET_NULL, input.canConnectWithReason_(null)); + assertEquals(Blockly.Connection.REASON_TARGET_NULL, + input.canConnectWithReason_(null)); connectionTest_tearDown(); } @@ -62,9 +67,11 @@ function testCanConnectWithReason_TargetNull() { function testCanConnectWithReason_Disconnect() { connectionTest_setUp(); - var tempConnection = new Blockly.Connection({workspace: dummyWorkspace}, Blockly.OUTPUT_VALUE); + var tempConnection = new Blockly.Connection({workspace: dummyWorkspace}, + Blockly.OUTPUT_VALUE); Blockly.Connection.connectReciprocally(input, tempConnection); - assertEquals(Blockly.Connection.REASON_MUST_DISCONNECT, input.canConnectWithReason_(output)); + assertEquals(Blockly.Connection.REASON_MUST_DISCONNECT, + input.canConnectWithReason_(output)); connectionTest_tearDown(); } @@ -73,9 +80,11 @@ function testCanConnectWithReason_DifferentWorkspaces() { connectionTest_setUp(); input = new Blockly.Connection({workspace: {}}, Blockly.INPUT_VALUE); - output = new Blockly.Connection({workspace: dummyWorkspace}, Blockly.OUTPUT_VALUE); + output = new Blockly.Connection({workspace: dummyWorkspace}, + Blockly.OUTPUT_VALUE); - assertEquals(Blockly.Connection.REASON_DIFFERENT_WORKSPACES, input.canConnectWithReason_(output)); + assertEquals(Blockly.Connection.REASON_DIFFERENT_WORKSPACES, + input.canConnectWithReason_(output)); connectionTest_tearDown(); } @@ -86,7 +95,8 @@ function testCanConnectWithReason_Self() { var block = {type_: "test block"}; input.sourceBlock_ = block; - assertEquals(Blockly.Connection.REASON_SELF_CONNECTION, input.canConnectWithReason_(input)); + assertEquals(Blockly.Connection.REASON_SELF_CONNECTION, + input.canConnectWithReason_(input)); connectionTest_tearDown(); } @@ -94,17 +104,25 @@ function testCanConnectWithReason_Self() { function testCanConnectWithReason_Type() { connectionTest_setUp(); - assertEquals(Blockly.Connection.REASON_WRONG_TYPE, input.canConnectWithReason_(previous)); - assertEquals(Blockly.Connection.REASON_WRONG_TYPE, input.canConnectWithReason_(next)); + assertEquals(Blockly.Connection.REASON_WRONG_TYPE, + input.canConnectWithReason_(previous)); + assertEquals(Blockly.Connection.REASON_WRONG_TYPE, + input.canConnectWithReason_(next)); - assertEquals(Blockly.Connection.REASON_WRONG_TYPE, output.canConnectWithReason_(previous)); - assertEquals(Blockly.Connection.REASON_WRONG_TYPE, output.canConnectWithReason_(next)); + assertEquals(Blockly.Connection.REASON_WRONG_TYPE, + output.canConnectWithReason_(previous)); + assertEquals(Blockly.Connection.REASON_WRONG_TYPE, + output.canConnectWithReason_(next)); - assertEquals(Blockly.Connection.REASON_WRONG_TYPE, previous.canConnectWithReason_(input)); - assertEquals(Blockly.Connection.REASON_WRONG_TYPE, previous.canConnectWithReason_(output)); + assertEquals(Blockly.Connection.REASON_WRONG_TYPE, + previous.canConnectWithReason_(input)); + assertEquals(Blockly.Connection.REASON_WRONG_TYPE, + previous.canConnectWithReason_(output)); - assertEquals(Blockly.Connection.REASON_WRONG_TYPE, next.canConnectWithReason_(input)); - assertEquals(Blockly.Connection.REASON_WRONG_TYPE, next.canConnectWithReason_(output)); + assertEquals(Blockly.Connection.REASON_WRONG_TYPE, + next.canConnectWithReason_(input)); + assertEquals(Blockly.Connection.REASON_WRONG_TYPE, + next.canConnectWithReason_(output)); connectionTest_tearDown(); } @@ -112,17 +130,21 @@ function testCanConnectWithReason_Type() { function testCanConnectWithReason_CanConnect() { connectionTest_setUp(); - assertEquals(Blockly.Connection.CAN_CONNECT, previous.canConnectWithReason_(next)); - assertEquals(Blockly.Connection.CAN_CONNECT, next.canConnectWithReason_(previous)); - assertEquals(Blockly.Connection.CAN_CONNECT, input.canConnectWithReason_(output)); - assertEquals(Blockly.Connection.CAN_CONNECT, output.canConnectWithReason_(input)); + assertEquals(Blockly.Connection.CAN_CONNECT, + previous.canConnectWithReason_(next)); + assertEquals(Blockly.Connection.CAN_CONNECT, + next.canConnectWithReason_(previous)); + assertEquals(Blockly.Connection.CAN_CONNECT, + input.canConnectWithReason_(output)); + assertEquals(Blockly.Connection.CAN_CONNECT, + output.canConnectWithReason_(input)); connectionTest_tearDown(); } /** - * The next set of tests checks that exceptions are being thrown at the correct times (external - * view of errors). + * The next set of tests checks that exceptions are being thrown at the correct + * times (external view of errors). */ function testCheckConnection_Self() { connectionTest_setUp(); @@ -222,6 +244,40 @@ function testCheckConnection_TypeNextOutput() { connectionTest_tearDown(); } +function test_isConnectionAllowed() { + var sharedWorkspace = {}; + // Two connections of opposite types near each other. + var one = helper_createConnection(5 /* x */, 10 /* y */, + Blockly.INPUT_VALUE); + one.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace); + + var two = helper_createConnection(10 /* x */, 15 /* y */, + Blockly.OUTPUT_VALUE); + two.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace); + + assertTrue(one.isConnectionAllowed(two, 20.0)); + // Move connections farther apart. + two.x_ = 100; + two.y_ = 100; + assertFalse(one.isConnectionAllowed(two, 20.0)); + + // Don't offer to connect an already connected left (male) value plug to + // an available right (female) value plug. + var three = helper_createConnection(0, 0, Blockly.OUTPUT_VALUE); + three.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace); + + assertTrue(one.isConnectionAllowed(three, 20.0)); + var four = helper_createConnection(0, 0, Blockly.INPUT_VALUE); + four.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace); + + Blockly.Connection.connectReciprocally(three, four); + assertFalse(one.isConnectionAllowed(three, 20.0)); + + // Don't connect two connections on the same block. + two.sourceBlock_ = one.sourceBlock_; + assertFalse(one.isConnectionAllowed(two, 1000.0)); +} + function testCheckConnection_Okay() { connectionTest_setUp(); previous.checkConnection_(next); diff --git a/tests/jsunit/db_test.js b/tests/jsunit/db_test.js new file mode 100644 index 000000000..45fc62a4b --- /dev/null +++ b/tests/jsunit/db_test.js @@ -0,0 +1,76 @@ +/** + * @license + * Blockly Tests + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +'use strict'; + +function test_DB_getNeighbours() { + var db = new Blockly.ConnectionDB(); + + // Search an empty list. + assertEquals(helper_getNeighbours(db, 10 /* x */, 10 /* y */, 100 /* radius */).length, 0); + + // Set up some connections. + for (var i = 0; i < 10; i++) { + db.addConnection_(helper_createConnection(0, i, Blockly.PREVIOUS_STATEMENT)); + } + + // Test block belongs at beginning + var result = helper_getNeighbours(db, 0, 0, 4); + assertEquals(5, result.length); + for (i = 0; i < result.length; i++) { + assertNotEquals(result.indexOf(db[i]), -1); // contains + } + + // Test block belongs at middle + result = helper_getNeighbours(db, 0, 4, 2); + assertEquals(5, result.length); + for (i = 0; i < result.length; i++) { + assertNotEquals(result.indexOf(db[i + 2]), -1); // contains + } + + // Test block belongs at end + result = helper_getNeighbours(db, 0, 9, 4); + assertEquals(5, result.length); + for (i = 0; i < result.length; i++) { + assertNotEquals(result.indexOf(db[i + 5]), -1); // contains + } + + // Test block has no neighbours due to being out of range in the x direction + result = helper_getNeighbours(db, 10, 9, 4); + assertEquals(result.length, 0); + + // Test block has no neighbours due to being out of range in the y direction + result = helper_getNeighbours(db, 0, 19, 4); + assertEquals(result.length, 0); + + // Test block has no neighbours due to being out of range diagonally + result = helper_getNeighbours(db, -2, -2, 2); + assertEquals(result.length, 0); +} + +function helper_getNeighbours(db, x, y, radius) { + return db.getNeighbours(helper_createConnection(x, y, Blockly.NEXT_STATEMENT), radius); +} + +function helper_createConnection(x, y, type) { + var conn = new Blockly.Connection({workspace: {}}, type); + conn.x_ = x; + conn.y_ = y; + return conn; +} \ No newline at end of file