diff --git a/core/connection.js b/core/connection.js index fa4205620..595a84b86 100644 --- a/core/connection.js +++ b/core/connection.js @@ -152,6 +152,17 @@ 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. @@ -546,94 +557,99 @@ Blockly.Connection.prototype.tighten_ = function() { * 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_; + return {connection: null, radius: maxLimit}; + // if (this.targetConnection) { + // // Don't offer to connect to a connection that's already connected. + // return {connection: null, radius: maxLimit}; + // } + // // 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; + // // Since this connection is probably being dragged, add the delta. + // var currentX = this.x_ + dx; + // var currentY = this.y_ + dy; - // Find the closest y location. - var candidatePosition = db.findPositionForConnection_(this); + // // Find the closest y location. + // var candidatePosition = db.findPositionForConnection_(this); - // Walk forward and back on the y axis looking for the closest x,y point. - var pointerMin = candidatePosition; - var pointerMax = candidatePosition; - 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)); - } + // // Walk forward and back on the y axis looking for the closest x,y point. + // var pointerMin = candidatePosition; + // var pointerMax = candidatePosition; + // 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. + // /** + // * 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; - } + // // 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; - } + // // 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); + // // 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}; + // // 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}; }; /** diff --git a/core/connection_db.js b/core/connection_db.js index 157a46f51..bdbc012b3 100644 --- a/core/connection_db.js +++ b/core/connection_db.js @@ -204,6 +204,126 @@ Blockly.ConnectionDB.prototype.getNeighbours = function(connection, 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 {!{connection: ?Blockly.Connection, radius: number}} Contains two properties: 'connection' which is either + * 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 == 0) { + 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 (isConnectionAllowed(conn, 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 (isConnectionAllowed(conn, temp, bestRadius)) { + bestConnection = temp; + bestRadius = temp.distanceFrom(conn); + } + pointerMax++; + } + + // Reset the values of x and y. + conn.x_ = baseX; + conn.y_ = baseY; + return bestConnection; +}; + +// TODO: fenichel: consider moving this to connection.js +/** + * Check if the two connections can be dragged to connect to each other. + * @param {Blockly.Connection} moving The connection being dragged. + * @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.ConnectionDB.prototype.isConnectionAllowed = function(moving, candidate, maxRadius) { + if (moving.distanceFrom(candidate) > maxRadius) { + return false; + } + + // Type checking + var canConnect = moving.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) { + 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 true; + } + + // Don't let blocks try to connect to themselves or ones they nest. + var targetSourceBlock = candidate.sourceBlock_; + var sourceBlock = moving.sourceBlock_; + if (targetSourceBlock && sourceBlock) { + do { + if (sourceBlock == targetSourceBlock) { + return true; + } + targetSourceBlock = targetSourceBlock.getParent(); + } while (targetSourceBlock); + } + + return true; +}; + /** * Initialize a set of connection DBs for a specified workspace. * @param {!Blockly.Workspace} workspace The workspace this DB is for. diff --git a/tests/jsunit/connection_db_test.js b/tests/jsunit/connection_db_test.js index cd60bf06d..c0be95b70 100644 --- a/tests/jsunit/connection_db_test.js +++ b/tests/jsunit/connection_db_test.js @@ -200,10 +200,68 @@ function test_DB_ordering() { } } +function test_DB_isConnectionAllowed() { + var db = new Blockly.ConnectionDB(); + 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(db.isConnectionAllowed(one, two, 20.0)); + // Move connections farther apart + two.x_ = 100; + two.y_ = 100; + assertFalse(db.isConnectionAllowed(one, 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(db.isConnectionAllowed(one, three, 20.0)); + var four = helper_createConnection(0, 0, Blockly.INPUT_VALUE); + four.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace); + + Blockly.Connection.connectReciprocally(three, four); + assertFalse(db.isConnectionAllowed(one, three, 20.0)); + + // Don't connect two connections on the same block + two.sourceBlock_ = one.sourceBlock_; + assertFalse(db.isConnectionAllowed(one, two, 1000.0)); +} + +// function test_DB_isConnectionAllowedNext() { +// var db = new Blockly.ConnectionDB(); +// var one = helper_createConnection(0, 0, Blockly.NEXT_STATEMENT); +// one.setInput(new Input.InputValue("test input", "" /* align */, null /* checks */)); + +// var two = helper_createConnection(0, 0, Blockly.NEXT_STATEMENT); +// two.setInput(new Input.InputValue("test input", "" /* align */, null /* checks */)); + +// // Don't offer to connect the bottom of a statement block to one that's already connected. +// varv three = helper_createConnection(0, 0, Blockly.PREVIOUS_STATEMENT); +// assertTrue(db.isConnectionAllowed(one, three, 20.0)); +// three.connectReciprocally_(two); +// assertFalse(db.isConnectionAllowed(one, three, 20.0)); +// } + function helper_getNeighbours(db, x, y, radius) { return db.getNeighbours(helper_createConnection(x, y, Blockly.NEXT_STATEMENT), radius); } +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) { var conn = new Blockly.Connection({workspace: {}}, type); conn.x_ = x;