Files
blockly/core/connection_checker.ts
Maribeth Bottorff 037eb59b89 chore: Lint TsDoc. (#6353)
* chore: add linting for tsdoc

* chore: don't require types on return

* chore: remove redundant fileoverview from ts

* chore: change return to returns and add some newlines

* chore: remove license tag

* chore: don't require params/return docs

* chore: remove spurious struct tags

* Revert "chore: change return to returns and add some newlines"

This reverts commit d6d8656a45.

* chore: don't auto-add param names

* chore: disable require-param bc it breaks on this

* return to returns and add line breaks

* chore: configure additional jsdoc rules

* chore: run format

* Revert "chore: remove license tag"

This reverts commit 173455588a.

* chore: allow license tag format

* chore: only require jsdoc on exported items

* chore: add missing jsdoc or silence where needed

* chore: run format

* chore: lint fixes
2022-08-23 14:27:22 -07:00

306 lines
10 KiB
TypeScript

/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* An object that encapsulates logic for checking whether a
* potential connection is safe and valid.
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ConnectionChecker');
import * as common from './common.js';
import {Connection} from './connection.js';
import {ConnectionType} from './connection_type.js';
import type {IConnectionChecker} from './interfaces/i_connection_checker.js';
import * as internalConstants from './internal_constants.js';
import * as registry from './registry.js';
import type {RenderedConnection} from './rendered_connection.js';
/**
* Class for connection type checking logic.
*
* @alias Blockly.ConnectionChecker
*/
export class ConnectionChecker implements IConnectionChecker {
/**
* Check whether the current connection can connect with the target
* connection.
*
* @param a Connection to check compatibility with.
* @param b Connection to check compatibility with.
* @param isDragging True if the connection is being made by dragging a block.
* @param opt_distance The max allowable distance between the connections for
* drag checks.
* @returns Whether the connection is legal.
*/
canConnect(
a: Connection|null, b: Connection|null, isDragging: boolean,
opt_distance?: number): boolean {
return this.canConnectWithReason(a, b, isDragging, opt_distance) ===
Connection.CAN_CONNECT;
}
/**
* Checks whether the current connection can connect with the target
* connection, and return an error code if there are problems.
*
* @param a Connection to check compatibility with.
* @param b Connection to check compatibility with.
* @param isDragging True if the connection is being made by dragging a block.
* @param opt_distance The max allowable distance between the connections for
* drag checks.
* @returns Connection.CAN_CONNECT if the connection is legal, an error code
* otherwise.
*/
canConnectWithReason(
a: Connection|null, b: Connection|null, isDragging: boolean,
opt_distance?: number): number {
const safety = this.doSafetyChecks(a, b);
if (safety !== Connection.CAN_CONNECT) {
return safety;
}
// If the safety checks passed, both connections are non-null.
const connOne = a!;
const connTwo = b!;
if (!this.doTypeChecks(connOne, connTwo)) {
return Connection.REASON_CHECKS_FAILED;
}
if (isDragging &&
!this.doDragChecks(
a as RenderedConnection, b as RenderedConnection,
opt_distance || 0)) {
return Connection.REASON_DRAG_CHECKS_FAILED;
}
return Connection.CAN_CONNECT;
}
/**
* Helper method that translates a connection error code into a string.
*
* @param errorCode The error code.
* @param a One of the two connections being checked.
* @param b The second of the two connections being checked.
* @returns A developer-readable error string.
*/
getErrorMessage(errorCode: number, a: Connection|null, b: Connection|null):
string {
switch (errorCode) {
case Connection.REASON_SELF_CONNECTION:
return 'Attempted to connect a block to itself.';
case Connection.REASON_DIFFERENT_WORKSPACES:
// Usually this means one block has been deleted.
return 'Blocks not on same workspace.';
case Connection.REASON_WRONG_TYPE:
return 'Attempt to connect incompatible types.';
case Connection.REASON_TARGET_NULL:
return 'Target connection is null.';
case Connection.REASON_CHECKS_FAILED: {
const connOne = a!;
const connTwo = b!;
let msg = 'Connection checks failed. ';
msg += connOne + ' expected ' + connOne.getCheck() + ', found ' +
connTwo.getCheck();
return msg;
}
case Connection.REASON_SHADOW_PARENT:
return 'Connecting non-shadow to shadow block.';
case Connection.REASON_DRAG_CHECKS_FAILED:
return 'Drag checks failed.';
case Connection.REASON_PREVIOUS_AND_OUTPUT:
return 'Block would have an output and a previous connection.';
default:
return 'Unknown connection failure: this should never happen!';
}
}
/**
* Check that connecting the given connections is safe, meaning that it would
* not break any of Blockly's basic assumptions (e.g. no self connections).
*
* @param a The first of the connections to check.
* @param b The second of the connections to check.
* @returns An enum with the reason this connection is safe or unsafe.
*/
doSafetyChecks(a: Connection|null, b: Connection|null): number {
if (!a || !b) {
return Connection.REASON_TARGET_NULL;
}
let superiorBlock;
let inferiorBlock;
let superiorConnection;
let inferiorConnection;
if (a.isSuperior()) {
superiorBlock = a.getSourceBlock();
inferiorBlock = b.getSourceBlock();
superiorConnection = a;
inferiorConnection = b;
} else {
inferiorBlock = a.getSourceBlock();
superiorBlock = b.getSourceBlock();
inferiorConnection = a;
superiorConnection = b;
}
if (superiorBlock === inferiorBlock) {
return Connection.REASON_SELF_CONNECTION;
} else if (
inferiorConnection.type !==
internalConstants.OPPOSITE_TYPE[superiorConnection.type]) {
return Connection.REASON_WRONG_TYPE;
} else if (superiorBlock.workspace !== inferiorBlock.workspace) {
return Connection.REASON_DIFFERENT_WORKSPACES;
} else if (superiorBlock.isShadow() && !inferiorBlock.isShadow()) {
return Connection.REASON_SHADOW_PARENT;
} else if (
inferiorConnection.type === ConnectionType.OUTPUT_VALUE &&
inferiorBlock.previousConnection &&
inferiorBlock.previousConnection.isConnected()) {
return Connection.REASON_PREVIOUS_AND_OUTPUT;
} else if (
inferiorConnection.type === ConnectionType.PREVIOUS_STATEMENT &&
inferiorBlock.outputConnection &&
inferiorBlock.outputConnection.isConnected()) {
return Connection.REASON_PREVIOUS_AND_OUTPUT;
}
return Connection.CAN_CONNECT;
}
/**
* Check whether this connection is compatible with another connection with
* respect to the value type system. E.g. square_root("Hello") is not
* compatible.
*
* @param a Connection to compare.
* @param b Connection to compare against.
* @returns True if the connections share a type.
*/
doTypeChecks(a: Connection, b: Connection): boolean {
const checkArrayOne = a.getCheck();
const checkArrayTwo = b.getCheck();
if (!checkArrayOne || !checkArrayTwo) {
// One or both sides are promiscuous enough that anything will fit.
return true;
}
// Find any intersection in the check lists.
for (let i = 0; i < checkArrayOne.length; i++) {
if (checkArrayTwo.indexOf(checkArrayOne[i]) !== -1) {
return true;
}
}
// No intersection.
return false;
}
/**
* Check whether this connection can be made by dragging.
*
* @param a Connection to compare.
* @param b Connection to compare against.
* @param distance The maximum allowable distance between connections.
* @returns True if the connection is allowed during a drag.
*/
doDragChecks(a: RenderedConnection, b: RenderedConnection, distance: number):
boolean {
if (a.distanceFrom(b) > distance) {
return false;
}
// Don't consider insertion markers.
if (b.getSourceBlock().isInsertionMarker()) {
return false;
}
switch (b.type) {
case ConnectionType.PREVIOUS_STATEMENT:
return this.canConnectToPrevious_(a, b);
case ConnectionType.OUTPUT_VALUE: {
// Don't offer to connect an already connected left (male) value plug to
// an available right (female) value plug.
if (b.isConnected() && !b.targetBlock()!.isInsertionMarker() ||
a.isConnected()) {
return false;
}
break;
}
case ConnectionType.INPUT_VALUE: {
// 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 immovable block.
if (b.isConnected() && !b.targetBlock()!.isMovable() &&
!b.targetBlock()!.isShadow()) {
return false;
}
break;
}
case ConnectionType.NEXT_STATEMENT: {
// Don't let a block with no next connection bump other blocks out of
// the stack. But covering up a shadow block or stack of shadow blocks
// is fine. Similarly, replacing a terminal statement with another
// terminal statement is allowed.
if (b.isConnected() && !a.getSourceBlock().nextConnection &&
!b.targetBlock()!.isShadow() && b.targetBlock()!.nextConnection) {
return false;
}
break;
}
default:
// Unexpected connection type.
return false;
}
// Don't let blocks try to connect to themselves or ones they nest.
if (common.draggingConnections.indexOf(b) !== -1) {
return false;
}
return true;
}
/**
* Helper function for drag checking.
*
* @param a The connection to check, which must be a statement input or next
* connection.
* @param b A nearby connection to check, which must be a previous connection.
* @returns True if the connection is allowed, false otherwise.
*/
protected canConnectToPrevious_(a: Connection, b: Connection): boolean {
if (a.targetConnection) {
// This connection is already occupied.
// A next connection will never disconnect itself mid-drag.
return false;
}
// Don't let blocks try to connect to themselves or ones they nest.
if (common.draggingConnections.indexOf(b) !== -1) {
return false;
}
if (!b.targetConnection) {
return true;
}
const targetBlock = b.targetBlock();
// If it is connected to a real block, game over.
if (!targetBlock!.isInsertionMarker()) {
return false;
}
// If it's connected to an insertion marker but that insertion marker
// is the first block in a stack, it's still fine. If that insertion
// marker is in the middle of a stack, it won't work.
return !targetBlock!.getPreviousBlock();
}
}
registry.register(
registry.Type.CONNECTION_CHECKER, registry.DEFAULT, ConnectionChecker);