summaryrefslogtreecommitdiff
path: root/blockly/core/connection_db.js
blob: 8b3c3008e3c5a85932f7016e16067f715b9e16b3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
/**
 * @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.
 */
Blockly.ConnectionDB.prototype.addConnection = function(connection) {
  if (connection.inDB_) {
    throw 'Connection already in database.';
  }
  if (connection.getSourceBlock().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 {number} The index of the connection, or -1 if the connection was
 *     not found.
 */
Blockly.ConnectionDB.prototype.findConnection = function(conn) {
  if (!this.length) {
    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) {
  /* eslint-disable indent */
  if (!this.length) {
    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;
};  /* eslint-enable indent */

/**
 * 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.<Blockly.Connection>} List of connections.
 */
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);
  }

  var neighbours = [];
  /**
   * 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;
  }

  // Walk forward and back on the y axis looking for the closest x,y point.
  pointerMin = pointerMid;
  pointerMax = pointerMid;
  if (db.length) {
    while (pointerMin >= 0 && checkConnection_(pointerMin)) {
      pointerMin--;
    }
    do {
      pointerMax++;
    } while (pointerMax < db.length && checkConnection_(pointerMax));
  }

  return neighbours;
};


/**
 * Is the candidate connection close to the reference connection.
 * Extremely fast; only looks at Y distance.
 * @param {number} index Index in database of candidate connection.
 * @param {number} baseY Reference connection's Y value.
 * @param {number} maxRadius The maximum radius to another connection.
 * @return {boolean} True if connection is in range.
 * @private
 */
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 {!goog.math.Coordinate} dxy 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,
    dxy) {
  // Don't bother.
  if (!this.length) {
    return {connection: null, radius: maxRadius};
  }

  // Stash the values of x and y from before the drag.
  var baseY = conn.y_;
  var baseX = conn.x_;

  conn.x_ = baseX + dxy.x;
  conn.y_ = baseY + dxy.y;

  // 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;

  // If there were no valid connections, bestConnection will be null.
  return {connection: bestConnection, radius: bestRadius};
};

/**
 * 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;
};