/** * @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. */ 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 (on the block that's being dragged). * @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; } // Don't offer to splice into a stack where the connected block is // immovable. if (b.targetBlock() && !b.targetBlock()!.isMovable()) { 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);