2013-10-30 14:46:03 -07:00
|
|
|
/**
|
2014-01-28 03:00:09 -08:00
|
|
|
* @license
|
2013-10-30 14:46:03 -07:00
|
|
|
* Visual Blocks Editor
|
|
|
|
*
|
|
|
|
* Copyright 2011 Google Inc.
|
2014-10-07 13:09:55 -07:00
|
|
|
* https://developers.google.com/blockly/
|
2013-10-30 14:46:03 -07:00
|
|
|
*
|
|
|
|
* 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 creating connections between blocks.
|
|
|
|
* @author fraser@google.com (Neil Fraser)
|
|
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
goog.provide('Blockly.Connection');
|
|
|
|
goog.provide('Blockly.ConnectionDB');
|
|
|
|
|
2015-02-06 15:27:25 -08:00
|
|
|
goog.require('goog.dom');
|
|
|
|
|
2013-10-30 14:46:03 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Class for a connection between blocks.
|
|
|
|
* @param {!Blockly.Block} source The block establishing this connection.
|
|
|
|
* @param {number} type The type of the connection.
|
|
|
|
* @constructor
|
|
|
|
*/
|
|
|
|
Blockly.Connection = function(source, type) {
|
2015-07-27 14:18:43 -05:00
|
|
|
/** @type {!Blockly.Block} */
|
2013-10-30 14:46:03 -07:00
|
|
|
this.sourceBlock_ = source;
|
2015-07-13 16:35:54 -05:00
|
|
|
/** @type {number} */
|
2013-10-30 14:46:03 -07:00
|
|
|
this.type = type;
|
|
|
|
// Shortcut for the databases for this connection's workspace.
|
2015-01-26 04:56:58 -08:00
|
|
|
this.dbList_ = source.workspace.connectionDBList;
|
|
|
|
this.hidden_ = !this.dbList_;
|
2013-10-30 14:46:03 -07:00
|
|
|
};
|
|
|
|
|
2015-09-12 18:44:14 -07:00
|
|
|
/**
|
|
|
|
* Connection this connection connects to. Null if not connected.
|
|
|
|
* @type {Blockly.Connection}
|
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.targetConnection = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* List of compatible value types. Null if all types are compatible.
|
|
|
|
* @type {Array}
|
2015-09-12 19:31:22 -07:00
|
|
|
* @private
|
2015-09-12 18:44:14 -07:00
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.check_ = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Horizontal location of this connection.
|
|
|
|
* @type {number}
|
2015-09-12 19:31:22 -07:00
|
|
|
* @private
|
2015-09-12 18:44:14 -07:00
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.x_ = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Vertical location of this connection.
|
|
|
|
* @type {number}
|
2015-09-12 19:31:22 -07:00
|
|
|
* @private
|
2015-09-12 18:44:14 -07:00
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.y_ = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Has this connection been added to the connection database?
|
|
|
|
* @type {boolean}
|
2015-09-12 19:31:22 -07:00
|
|
|
* @private
|
2015-09-12 18:44:14 -07:00
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.inDB_ = false;
|
|
|
|
|
2013-10-30 14:46:03 -07:00
|
|
|
/**
|
|
|
|
* Sever all links to this connection (not including from the source object).
|
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.dispose = function() {
|
|
|
|
if (this.targetConnection) {
|
|
|
|
throw 'Disconnect connection before disposing of it.';
|
|
|
|
}
|
|
|
|
if (this.inDB_) {
|
|
|
|
this.dbList_[this.type].removeConnection_(this);
|
|
|
|
}
|
|
|
|
if (Blockly.highlightedConnection_ == this) {
|
|
|
|
Blockly.highlightedConnection_ = null;
|
|
|
|
}
|
|
|
|
if (Blockly.localConnection_ == this) {
|
|
|
|
Blockly.localConnection_ = null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Does the connection belong to a superior block (higher in the source stack)?
|
|
|
|
* @return {boolean} True if connection faces down or right.
|
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.isSuperior = function() {
|
|
|
|
return this.type == Blockly.INPUT_VALUE ||
|
|
|
|
this.type == Blockly.NEXT_STATEMENT;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Connect this connection to another connection.
|
|
|
|
* @param {!Blockly.Connection} otherConnection Connection to connect to.
|
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.connect = function(otherConnection) {
|
|
|
|
if (this.sourceBlock_ == otherConnection.sourceBlock_) {
|
|
|
|
throw 'Attempted to connect a block to itself.';
|
|
|
|
}
|
|
|
|
if (this.sourceBlock_.workspace !== otherConnection.sourceBlock_.workspace) {
|
|
|
|
throw 'Blocks are on different workspaces.';
|
|
|
|
}
|
|
|
|
if (Blockly.OPPOSITE_TYPE[this.type] != otherConnection.type) {
|
|
|
|
throw 'Attempt to connect incompatible types.';
|
|
|
|
}
|
|
|
|
if (this.type == Blockly.INPUT_VALUE || this.type == Blockly.OUTPUT_VALUE) {
|
|
|
|
if (this.targetConnection) {
|
|
|
|
// Can't make a value connection if male block is already connected.
|
|
|
|
throw 'Source connection already connected (value).';
|
|
|
|
} else if (otherConnection.targetConnection) {
|
|
|
|
// If female block is already connected, disconnect and bump the male.
|
|
|
|
var orphanBlock = otherConnection.targetBlock();
|
|
|
|
orphanBlock.setParent(null);
|
|
|
|
if (!orphanBlock.outputConnection) {
|
|
|
|
throw 'Orphan block does not have an output connection.';
|
|
|
|
}
|
|
|
|
// Attempt to reattach the orphan at the end of the newly inserted
|
|
|
|
// block. Since this block may be a row, walk down to the end.
|
|
|
|
var newBlock = this.sourceBlock_;
|
|
|
|
var connection;
|
2014-11-04 12:27:27 -08:00
|
|
|
while (connection = Blockly.Connection.singleConnection_(
|
2013-10-30 14:46:03 -07:00
|
|
|
/** @type {!Blockly.Block} */ (newBlock), orphanBlock)) {
|
|
|
|
// '=' is intentional in line above.
|
|
|
|
if (connection.targetBlock()) {
|
|
|
|
newBlock = connection.targetBlock();
|
|
|
|
} else {
|
|
|
|
connection.connect(orphanBlock.outputConnection);
|
|
|
|
orphanBlock = null;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (orphanBlock) {
|
|
|
|
// Unable to reattach orphan. Bump it off to the side.
|
2014-12-23 16:44:00 -08:00
|
|
|
setTimeout(function() {
|
2013-10-30 14:46:03 -07:00
|
|
|
orphanBlock.outputConnection.bumpAwayFrom_(otherConnection);
|
|
|
|
}, Blockly.BUMP_DELAY);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (this.targetConnection) {
|
|
|
|
throw 'Source connection already connected (block).';
|
|
|
|
} else if (otherConnection.targetConnection) {
|
|
|
|
// Statement blocks may be inserted into the middle of a stack.
|
|
|
|
if (this.type != Blockly.PREVIOUS_STATEMENT) {
|
|
|
|
throw 'Can only do a mid-stack connection with the top of a block.';
|
|
|
|
}
|
|
|
|
// Split the stack.
|
|
|
|
var orphanBlock = otherConnection.targetBlock();
|
|
|
|
orphanBlock.setParent(null);
|
|
|
|
if (!orphanBlock.previousConnection) {
|
|
|
|
throw 'Orphan block does not have a previous connection.';
|
|
|
|
}
|
|
|
|
// Attempt to reattach the orphan at the bottom of the newly inserted
|
|
|
|
// block. Since this block may be a stack, walk down to the end.
|
|
|
|
var newBlock = this.sourceBlock_;
|
|
|
|
while (newBlock.nextConnection) {
|
|
|
|
if (newBlock.nextConnection.targetConnection) {
|
2014-09-08 14:26:52 -07:00
|
|
|
newBlock = newBlock.getNextBlock();
|
2013-10-30 14:46:03 -07:00
|
|
|
} else {
|
2014-11-04 12:27:27 -08:00
|
|
|
if (orphanBlock.previousConnection.checkType_(
|
|
|
|
newBlock.nextConnection)) {
|
|
|
|
newBlock.nextConnection.connect(orphanBlock.previousConnection);
|
|
|
|
orphanBlock = null;
|
|
|
|
}
|
2013-10-30 14:46:03 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (orphanBlock) {
|
|
|
|
// Unable to reattach orphan. Bump it off to the side.
|
2014-12-23 16:44:00 -08:00
|
|
|
setTimeout(function() {
|
2013-10-30 14:46:03 -07:00
|
|
|
orphanBlock.previousConnection.bumpAwayFrom_(otherConnection);
|
|
|
|
}, Blockly.BUMP_DELAY);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine which block is superior (higher in the source stack).
|
|
|
|
var parentBlock, childBlock;
|
|
|
|
if (this.isSuperior()) {
|
|
|
|
// Superior block.
|
|
|
|
parentBlock = this.sourceBlock_;
|
|
|
|
childBlock = otherConnection.sourceBlock_;
|
|
|
|
} else {
|
|
|
|
// Inferior block.
|
|
|
|
parentBlock = otherConnection.sourceBlock_;
|
|
|
|
childBlock = this.sourceBlock_;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Establish the connections.
|
|
|
|
this.targetConnection = otherConnection;
|
|
|
|
otherConnection.targetConnection = this;
|
|
|
|
|
|
|
|
// Demote the inferior block so that one is a child of the superior one.
|
|
|
|
childBlock.setParent(parentBlock);
|
|
|
|
|
|
|
|
if (parentBlock.rendered) {
|
2014-12-23 11:22:02 -08:00
|
|
|
parentBlock.updateDisabled();
|
2013-10-30 14:46:03 -07:00
|
|
|
}
|
|
|
|
if (childBlock.rendered) {
|
2014-12-23 11:22:02 -08:00
|
|
|
childBlock.updateDisabled();
|
2013-10-30 14:46:03 -07:00
|
|
|
}
|
|
|
|
if (parentBlock.rendered && childBlock.rendered) {
|
|
|
|
if (this.type == Blockly.NEXT_STATEMENT ||
|
|
|
|
this.type == Blockly.PREVIOUS_STATEMENT) {
|
|
|
|
// Child block may need to square off its corners if it is in a stack.
|
|
|
|
// Rendering a child will render its parent.
|
|
|
|
childBlock.render();
|
|
|
|
} else {
|
|
|
|
// Child block does not change shape. Rendering the parent node will
|
|
|
|
// move its connected children into position.
|
|
|
|
parentBlock.render();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Does the given block have one and only one connection point that will accept
|
|
|
|
* the orphaned block?
|
|
|
|
* @param {!Blockly.Block} block The superior block.
|
|
|
|
* @param {!Blockly.Block} orphanBlock The inferior block.
|
|
|
|
* @return {Blockly.Connection} The suitable connection point on 'block',
|
|
|
|
* or null.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Blockly.Connection.singleConnection_ = function(block, orphanBlock) {
|
|
|
|
var connection = false;
|
|
|
|
for (var x = 0; x < block.inputList.length; x++) {
|
|
|
|
var thisConnection = block.inputList[x].connection;
|
|
|
|
if (thisConnection && thisConnection.type == Blockly.INPUT_VALUE &&
|
|
|
|
orphanBlock.outputConnection.checkType_(thisConnection)) {
|
|
|
|
if (connection) {
|
|
|
|
return null; // More than one connection.
|
|
|
|
}
|
|
|
|
connection = thisConnection;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return connection;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Disconnect this connection.
|
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.disconnect = function() {
|
|
|
|
var otherConnection = this.targetConnection;
|
|
|
|
if (!otherConnection) {
|
|
|
|
throw 'Source connection not connected.';
|
|
|
|
} else if (otherConnection.targetConnection != this) {
|
|
|
|
throw 'Target connection not connected to source connection.';
|
|
|
|
}
|
|
|
|
otherConnection.targetConnection = null;
|
|
|
|
this.targetConnection = null;
|
|
|
|
|
|
|
|
// Rerender the parent so that it may reflow.
|
|
|
|
var parentBlock, childBlock;
|
|
|
|
if (this.isSuperior()) {
|
|
|
|
// Superior block.
|
|
|
|
parentBlock = this.sourceBlock_;
|
|
|
|
childBlock = otherConnection.sourceBlock_;
|
|
|
|
} else {
|
|
|
|
// Inferior block.
|
|
|
|
parentBlock = otherConnection.sourceBlock_;
|
|
|
|
childBlock = this.sourceBlock_;
|
|
|
|
}
|
|
|
|
if (parentBlock.rendered) {
|
|
|
|
parentBlock.render();
|
|
|
|
}
|
|
|
|
if (childBlock.rendered) {
|
2014-12-23 11:22:02 -08:00
|
|
|
childBlock.updateDisabled();
|
2013-10-30 14:46:03 -07:00
|
|
|
childBlock.render();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the block that this connection connects to.
|
|
|
|
* @return {Blockly.Block} The connected block or null if none is connected.
|
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.targetBlock = function() {
|
|
|
|
if (this.targetConnection) {
|
|
|
|
return this.targetConnection.sourceBlock_;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Move the block(s) belonging to the connection to a point where they don't
|
|
|
|
* visually interfere with the specified connection.
|
|
|
|
* @param {!Blockly.Connection} staticConnection The connection to move away
|
|
|
|
* from.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.bumpAwayFrom_ = function(staticConnection) {
|
2014-12-23 11:22:02 -08:00
|
|
|
if (Blockly.dragMode_ != 0) {
|
2013-10-30 14:46:03 -07:00
|
|
|
// Don't move blocks around while the user is doing the same.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Move the root block.
|
|
|
|
var rootBlock = this.sourceBlock_.getRootBlock();
|
|
|
|
if (rootBlock.isInFlyout) {
|
|
|
|
// Don't move blocks around in a flyout.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
var reverse = false;
|
|
|
|
if (!rootBlock.isMovable()) {
|
|
|
|
// Can't bump an uneditable block away.
|
|
|
|
// Check to see if the other block is movable.
|
|
|
|
rootBlock = staticConnection.sourceBlock_.getRootBlock();
|
|
|
|
if (!rootBlock.isMovable()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Swap the connections and move the 'static' connection instead.
|
|
|
|
staticConnection = this;
|
|
|
|
reverse = true;
|
|
|
|
}
|
|
|
|
// Raise it to the top for extra visibility.
|
|
|
|
rootBlock.getSvgRoot().parentNode.appendChild(rootBlock.getSvgRoot());
|
|
|
|
var dx = (staticConnection.x_ + Blockly.SNAP_RADIUS) - this.x_;
|
2014-09-08 14:26:52 -07:00
|
|
|
var dy = (staticConnection.y_ + Blockly.SNAP_RADIUS) - this.y_;
|
2013-10-30 14:46:03 -07:00
|
|
|
if (reverse) {
|
|
|
|
// When reversing a bump due to an uneditable block, bump up.
|
|
|
|
dy = -dy;
|
|
|
|
}
|
2015-04-28 13:51:25 -07:00
|
|
|
if (rootBlock.RTL) {
|
2013-10-30 14:46:03 -07:00
|
|
|
dx = -dx;
|
|
|
|
}
|
|
|
|
rootBlock.moveBy(dx, dy);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Change the connection's coordinates.
|
|
|
|
* @param {number} x New absolute x coordinate.
|
|
|
|
* @param {number} y New absolute y coordinate.
|
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.moveTo = function(x, y) {
|
|
|
|
// Remove it from its old location in the database (if already present)
|
|
|
|
if (this.inDB_) {
|
|
|
|
this.dbList_[this.type].removeConnection_(this);
|
|
|
|
}
|
|
|
|
this.x_ = x;
|
|
|
|
this.y_ = y;
|
|
|
|
// Insert it into its new location in the database.
|
2015-01-26 04:56:58 -08:00
|
|
|
if (!this.hidden_) {
|
2015-01-26 02:45:03 -08:00
|
|
|
this.dbList_[this.type].addConnection_(this);
|
|
|
|
}
|
2013-10-30 14:46:03 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Change the connection's coordinates.
|
|
|
|
* @param {number} dx Change to x coordinate.
|
|
|
|
* @param {number} dy Change to y coordinate.
|
|
|
|
*/
|
|
|
|
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) {
|
2015-04-28 13:51:25 -07:00
|
|
|
var tabWidth = this.sourceBlock_.RTL ? -Blockly.BlockSvg.TAB_WIDTH :
|
|
|
|
Blockly.BlockSvg.TAB_WIDTH;
|
2013-10-30 14:46:03 -07:00
|
|
|
steps = 'm 0,0 v 5 c 0,10 ' + -tabWidth + ',-8 ' + -tabWidth + ',7.5 s ' +
|
|
|
|
tabWidth + ',-2.5 ' + tabWidth + ',7.5 v 5';
|
|
|
|
} else {
|
2015-04-28 13:51:25 -07:00
|
|
|
if (this.sourceBlock_.RTL) {
|
2014-12-01 11:21:10 -08:00
|
|
|
steps = 'm 20,0 h -5 ' + Blockly.BlockSvg.NOTCH_PATH_RIGHT + ' h -5';
|
2013-10-30 14:46:03 -07:00
|
|
|
} else {
|
2014-12-01 11:21:10 -08:00
|
|
|
steps = 'm -20,0 h 5 ' + Blockly.BlockSvg.NOTCH_PATH_LEFT + ' h 5';
|
2013-10-30 14:46:03 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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,
|
2015-08-18 11:34:22 -07:00
|
|
|
transform: 'translate(' + x + ',' + y + ')'},
|
2013-10-30 14:46:03 -07:00
|
|
|
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
|
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.tighten_ = function() {
|
2015-05-27 18:54:37 -07:00
|
|
|
var dx = this.targetConnection.x_ - this.x_;
|
|
|
|
var dy = this.targetConnection.y_ - this.y_;
|
2013-10-30 14:46:03 -07:00
|
|
|
if (dx != 0 || dy != 0) {
|
|
|
|
var block = this.targetBlock();
|
|
|
|
var svgRoot = block.getSvgRoot();
|
|
|
|
if (!svgRoot) {
|
|
|
|
throw 'block is not rendered.';
|
|
|
|
}
|
|
|
|
var xy = Blockly.getRelativeXY_(svgRoot);
|
|
|
|
block.getSvgRoot().setAttribute('transform',
|
2015-08-18 11:34:22 -07:00
|
|
|
'translate(' + (xy.x - dx) + ',' + (xy.y - dy) + ')');
|
2013-10-30 14:46:03 -07:00
|
|
|
block.moveConnections_(-dx, -dy);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find the closest compatible connection to this connection.
|
|
|
|
* @param {number} maxLimit 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 {!Object} 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};
|
|
|
|
}
|
|
|
|
// Determine the opposite type of connection.
|
|
|
|
var oppositeType = Blockly.OPPOSITE_TYPE[this.type];
|
|
|
|
var db = this.dbList_[oppositeType];
|
|
|
|
|
|
|
|
// 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.
|
2014-11-04 12:27:27 -08:00
|
|
|
* @private
|
2013-10-30 14:46:03 -07:00
|
|
|
*/
|
|
|
|
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.
|
2014-09-08 14:26:52 -07:00
|
|
|
|
2013-10-30 14:46:03 -07:00
|
|
|
// Offering to connect the left (male) of a value block to an already
|
|
|
|
// connected value pair is ok, we'll splice it in.
|
2014-09-08 14:26:52 -07:00
|
|
|
// However, don't offer to splice into an unmovable block.
|
|
|
|
if (connection.type == Blockly.INPUT_VALUE &&
|
|
|
|
connection.targetConnection &&
|
|
|
|
!connection.targetBlock().isMovable()) {
|
|
|
|
return true;
|
|
|
|
}
|
2013-10-30 14:46:03 -07:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
2014-11-04 12:27:27 -08:00
|
|
|
// Only connections within the maxLimit radius.
|
2013-10-30 14:46:03 -07:00
|
|
|
var dx = currentX - db[yIndex].x_;
|
|
|
|
var dy = currentY - db[yIndex].y_;
|
|
|
|
var r = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (r <= maxLimit) {
|
|
|
|
closestConnection = db[yIndex];
|
|
|
|
maxLimit = r;
|
|
|
|
}
|
|
|
|
return dy < maxLimit;
|
|
|
|
}
|
|
|
|
return {connection: closestConnection, radius: maxLimit};
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Is this connection compatible with another connection with respect to the
|
|
|
|
* value type system. E.g. square_root("Hello") is not compatible.
|
|
|
|
* @param {!Blockly.Connection} otherConnection Connection to compare against.
|
|
|
|
* @return {boolean} True if the connections share a type.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.checkType_ = function(otherConnection) {
|
2015-02-25 18:07:04 -08:00
|
|
|
// Don't split a connection where both sides are immovable.
|
|
|
|
var thisTargetBlock = this.targetBlock();
|
|
|
|
if (thisTargetBlock && !thisTargetBlock.isMovable() &&
|
|
|
|
!this.sourceBlock_.isMovable()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
var otherTargetBlock = otherConnection.targetBlock();
|
|
|
|
if (otherTargetBlock && !otherTargetBlock.isMovable() &&
|
|
|
|
!otherConnection.sourceBlock_.isMovable()) {
|
|
|
|
return false;
|
|
|
|
}
|
2013-10-30 14:46:03 -07:00
|
|
|
if (!this.check_ || !otherConnection.check_) {
|
|
|
|
// One or both sides are promiscuous enough that anything will fit.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
// Find any intersection in the check lists.
|
|
|
|
for (var x = 0; x < this.check_.length; x++) {
|
|
|
|
if (otherConnection.check_.indexOf(this.check_[x]) != -1) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// No intersection.
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Change a connection's compatibility.
|
|
|
|
* @param {*} check Compatible value type or list of value types.
|
|
|
|
* Null if all types are compatible.
|
|
|
|
* @return {!Blockly.Connection} The connection being modified
|
|
|
|
* (to allow chaining).
|
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.setCheck = function(check) {
|
|
|
|
if (check) {
|
|
|
|
// Ensure that check is in an array.
|
2014-09-08 14:26:52 -07:00
|
|
|
if (!goog.isArray(check)) {
|
2013-10-30 14:46:03 -07:00
|
|
|
check = [check];
|
|
|
|
}
|
|
|
|
this.check_ = check;
|
|
|
|
// The new value type may not be compatible with the existing connection.
|
|
|
|
if (this.targetConnection && !this.checkType_(this.targetConnection)) {
|
|
|
|
if (this.isSuperior()) {
|
|
|
|
this.targetBlock().setParent(null);
|
|
|
|
} else {
|
|
|
|
this.sourceBlock_.setParent(null);
|
|
|
|
}
|
|
|
|
// Bump away.
|
|
|
|
this.sourceBlock_.bumpNeighbours_();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.check_ = null;
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find all nearby compatible connections to this connection.
|
|
|
|
* Type checking does not apply, since this function is used for bumping.
|
|
|
|
* @param {number} maxLimit The maximum radius to another connection.
|
|
|
|
* @return {!Array.<Blockly.Connection>} List of connections.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.neighbours_ = function(maxLimit) {
|
|
|
|
// Determine the opposite type of connection.
|
|
|
|
var oppositeType = Blockly.OPPOSITE_TYPE[this.type];
|
|
|
|
var db = this.dbList_[oppositeType];
|
|
|
|
|
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2015-02-27 16:11:04 -08:00
|
|
|
/**
|
|
|
|
* Set whether this connections is hidden (not tracked in a database) or not.
|
|
|
|
* @param {boolean} hidden True if connection is hidden.
|
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.setHidden = function(hidden) {
|
|
|
|
this.hidden_ = hidden;
|
|
|
|
if (hidden && this.inDB_) {
|
|
|
|
this.dbList_[this.type].removeConnection_(this);
|
|
|
|
} else if (!hidden && !this.inDB_) {
|
|
|
|
this.dbList_[this.type].addConnection_(this);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2013-10-30 14:46:03 -07:00
|
|
|
/**
|
|
|
|
* Hide this connection, as well as all down-stream connections on any block
|
|
|
|
* attached to this connection. This happens when a block is collapsed.
|
|
|
|
* Also hides down-stream comments.
|
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.hideAll = function() {
|
2015-02-27 16:11:04 -08:00
|
|
|
this.setHidden(true);
|
2013-10-30 14:46:03 -07:00
|
|
|
if (this.targetConnection) {
|
|
|
|
var blocks = this.targetBlock().getDescendants();
|
|
|
|
for (var b = 0; b < blocks.length; b++) {
|
|
|
|
var block = blocks[b];
|
|
|
|
// Hide all connections of all children.
|
|
|
|
var connections = block.getConnections_(true);
|
|
|
|
for (var c = 0; c < connections.length; c++) {
|
2015-02-27 16:11:04 -08:00
|
|
|
connections[c].setHidden(true);
|
2013-10-30 14:46:03 -07:00
|
|
|
}
|
|
|
|
// Close all bubbles of all children.
|
|
|
|
var icons = block.getIcons();
|
|
|
|
for (var x = 0; x < icons.length; x++) {
|
|
|
|
icons[x].setVisible(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unhide this connection, as well as all down-stream connections on any block
|
|
|
|
* attached to this connection. This happens when a block is expanded.
|
|
|
|
* Also unhides down-stream comments.
|
|
|
|
* @return {!Array.<!Blockly.Block>} List of blocks to render.
|
|
|
|
*/
|
|
|
|
Blockly.Connection.prototype.unhideAll = function() {
|
2015-02-27 16:11:04 -08:00
|
|
|
this.setHidden(false);
|
2013-10-30 14:46:03 -07:00
|
|
|
// All blocks that need unhiding must be unhidden before any rendering takes
|
|
|
|
// place, since rendering requires knowing the dimensions of lower blocks.
|
|
|
|
// Also, since rendering a block renders all its parents, we only need to
|
|
|
|
// render the leaf nodes.
|
|
|
|
var renderList = [];
|
|
|
|
if (this.type != Blockly.INPUT_VALUE && this.type != Blockly.NEXT_STATEMENT) {
|
|
|
|
// Only spider down.
|
|
|
|
return renderList;
|
|
|
|
}
|
|
|
|
var block = this.targetBlock();
|
|
|
|
if (block) {
|
|
|
|
var connections;
|
|
|
|
if (block.isCollapsed()) {
|
|
|
|
// This block should only be partially revealed since it is collapsed.
|
|
|
|
connections = [];
|
|
|
|
block.outputConnection && connections.push(block.outputConnection);
|
|
|
|
block.nextConnection && connections.push(block.nextConnection);
|
|
|
|
block.previousConnection && connections.push(block.previousConnection);
|
|
|
|
} else {
|
|
|
|
// Show all connections of this block.
|
|
|
|
connections = block.getConnections_(true);
|
|
|
|
}
|
|
|
|
for (var c = 0; c < connections.length; c++) {
|
2014-09-08 14:26:52 -07:00
|
|
|
renderList.push.apply(renderList, connections[c].unhideAll());
|
2013-10-30 14:46:03 -07:00
|
|
|
}
|
|
|
|
if (renderList.length == 0) {
|
|
|
|
// Leaf block.
|
|
|
|
renderList[0] = block;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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
|
|
|
|
*/
|
|
|
|
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.';
|
|
|
|
}
|
2015-02-03 16:33:16 -08:00
|
|
|
if (connection.sourceBlock_.isInFlyout) {
|
|
|
|
// Don't bother maintaining a database of connections in a flyout.
|
|
|
|
return;
|
|
|
|
}
|
2013-10-30 14:46:03 -07:00
|
|
|
// 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;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.';
|
|
|
|
}
|
|
|
|
connection.inDB_ = false;
|
|
|
|
// Find the connection using a binary search.
|
|
|
|
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;
|
|
|
|
};
|