Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add connection previewers #7825

Merged
merged 10 commits into from
Feb 2, 2024
257 changes: 218 additions & 39 deletions core/block_dragger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import * as common from './common.js';
import type {BlockMove} from './events/events_block_move.js';
import * as eventUtils from './events/utils.js';
import type {Icon} from './icons/icon.js';
import {InsertionMarkerManager} from './insertion_marker_manager.js';
import type {IBlockDragger} from './interfaces/i_block_dragger.js';
import type {IDragTarget} from './interfaces/i_drag_target.js';
import * as registry from './registry.js';
Expand All @@ -31,6 +30,26 @@ import type {WorkspaceSvg} from './workspace_svg.js';
import {hasBubble} from './interfaces/i_has_bubble.js';
import * as deprecation from './utils/deprecation.js';
import * as layers from './layers.js';
import {ConnectionType, IConnectionPreviewer} from './blockly.js';
import {RenderedConnection} from './rendered_connection.js';
import {config} from './config.js';
import {ComponentManager} from './component_manager.js';
import {IDeleteArea} from './interfaces/i_delete_area.js';
import {Connection} from './connection.js';
import {Block} from './block.js';
import {finishQueuedRenders} from './render_management.js';

/** Represents a nearby valid connection. */
interface ConnectionCandidate {
/** A connection on the dragging stack that is compatible with neighbour. */
local: RenderedConnection;

/** A nearby connection that is compatible with local. */
neighbour: RenderedConnection;

/** The distance between the local connection and the neighbour connection. */
distance: number;
}

/**
* Class for a block dragger. It moves blocks around the workspace when they
Expand All @@ -39,14 +58,17 @@ import * as layers from './layers.js';
export class BlockDragger implements IBlockDragger {
/** The top block in the stack that is being dragged. */
protected draggingBlock_: BlockSvg;
protected draggedConnectionManager_: InsertionMarkerManager;

protected connectionPreviewer: IConnectionPreviewer;

/** The workspace on which the block is being dragged. */
protected workspace_: WorkspaceSvg;

/** Which drag area the mouse pointer is over, if any. */
private dragTarget_: IDragTarget | null = null;

private connectionCandidate: ConnectionCandidate | null = null;

/** Whether the block would be deleted if dropped immediately. */
protected wouldDeleteBlock_ = false;
protected startXY_: Coordinate;
Expand All @@ -63,13 +85,13 @@ export class BlockDragger implements IBlockDragger {
*/
constructor(block: BlockSvg, workspace: WorkspaceSvg) {
this.draggingBlock_ = block;
this.workspace_ = workspace;

/** Object that keeps track of connections on dragged blocks. */
this.draggedConnectionManager_ = new InsertionMarkerManager(
this.draggingBlock_,
const previewerConstructor = registry.getClassFromOptions(
registry.Type.CONNECTION_PREVIEWER,
this.workspace_.options,
);

this.workspace_ = workspace;
this.connectionPreviewer = new previewerConstructor!(block);

/**
* The location of the top left corner of the dragging block at the
Expand All @@ -87,9 +109,7 @@ export class BlockDragger implements IBlockDragger {
*/
dispose() {
this.dragIconData_.length = 0;
if (this.draggedConnectionManager_) {
this.draggedConnectionManager_.dispose();
}
this.connectionPreviewer.dispose();
}

/**
Expand Down Expand Up @@ -155,7 +175,6 @@ export class BlockDragger implements IBlockDragger {

this.draggingBlock_.translate(newLoc.x, newLoc.y);
blockAnimation.disconnectUiEffect(this.draggingBlock_);
this.draggedConnectionManager_.updateAvailableConnections();
}

/** Fire a UI event at the start of a block drag. */
Expand All @@ -173,32 +192,178 @@ export class BlockDragger implements IBlockDragger {
* display accordingly.
*
* @param e The most recent move event.
* @param currentDragDeltaXY How far the pointer has moved from the position
* @param delta How far the pointer has moved from the position
* at the start of the drag, in pixel units.
*/
drag(e: PointerEvent, currentDragDeltaXY: Coordinate) {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
drag(e: PointerEvent, delta: Coordinate) {
const block = this.draggingBlock_;
this.moveBlock(block, delta);
this.updateDragTargets(e, block);
this.wouldDeleteBlock_ = this.wouldDeleteBlock(e, block, delta);
this.updateCursorDuringBlockDrag_();
this.updateConnectionPreview(block, delta);
}

private moveBlock(draggingBlock: BlockSvg, dragDelta: Coordinate) {
const delta = this.pixelsToWorkspaceUnits_(dragDelta);
const newLoc = Coordinate.sum(this.startXY_, delta);
this.draggingBlock_.moveDuringDrag(newLoc);
draggingBlock.moveDuringDrag(newLoc);
}

private updateDragTargets(e: PointerEvent, draggingBlock: BlockSvg) {
const newDragTarget = this.workspace_.getDragTarget(e);
if (this.dragTarget_ !== newDragTarget) {
this.dragTarget_?.onDragExit(draggingBlock);
newDragTarget?.onDragEnter(draggingBlock);
}
newDragTarget?.onDragOver(draggingBlock);
this.dragTarget_ = newDragTarget;
}

const oldDragTarget = this.dragTarget_;
this.dragTarget_ = this.workspace_.getDragTarget(e);
/**
* Returns true if we would delete the block if it was dropped at this time,
* false otherwise.
*/
private wouldDeleteBlock(
e: PointerEvent,
draggingBlock: BlockSvg,
delta: Coordinate,
): boolean {
const dragTarget = this.workspace_.getDragTarget(e);
if (!dragTarget) return false;

const componentManager = this.workspace_.getComponentManager();
const isDeleteArea = componentManager.hasCapability(
dragTarget.id,
ComponentManager.Capability.DELETE_AREA,
);
if (!isDeleteArea) return false;

return (dragTarget as IDeleteArea).wouldDelete(
draggingBlock,
!!this.getConnectionCandidate(draggingBlock, delta),
);
}

this.draggedConnectionManager_.update(delta, this.dragTarget_);
const oldWouldDeleteBlock = this.wouldDeleteBlock_;
this.wouldDeleteBlock_ = this.draggedConnectionManager_.wouldDeleteBlock;
if (oldWouldDeleteBlock !== this.wouldDeleteBlock_) {
// Prevent unnecessary add/remove class calls.
this.updateCursorDuringBlockDrag_();
private updateConnectionPreview(draggingBlock: BlockSvg, delta: Coordinate) {
const currCandidate = this.connectionCandidate;
const newCandidate = this.getConnectionCandidate(draggingBlock, delta);
if (!newCandidate) {
this.connectionPreviewer.hidePreview();
this.connectionCandidate = null;
return;
}
const candidate =
currCandidate &&
this.currCandidateIsBetter(currCandidate, delta, newCandidate)
? currCandidate
: newCandidate;
this.connectionCandidate = candidate;
const {local, neighbour} = candidate;
if (
(local.type === ConnectionType.OUTPUT_VALUE ||
local.type === ConnectionType.PREVIOUS_STATEMENT) &&
neighbour.isConnected() &&
!neighbour.targetBlock()!.isInsertionMarker() &&
!this.orphanCanConnectAtEnd(
draggingBlock,
neighbour.targetBlock()!,
local.type,
)
) {
this.connectionPreviewer.previewReplacement(
local,
neighbour,
neighbour.targetBlock()!,
);
return;
}
this.connectionPreviewer.previewConnection(local, neighbour);
}

// Call drag enter/exit/over after wouldDeleteBlock is called in
// InsertionMarkerManager.update.
if (this.dragTarget_ !== oldDragTarget) {
oldDragTarget && oldDragTarget.onDragExit(this.draggingBlock_);
this.dragTarget_ && this.dragTarget_.onDragEnter(this.draggingBlock_);
/**
* Returns true if the given orphan block can connect at the end of the
* top block's stack or row, false otherwise.
*/
private orphanCanConnectAtEnd(
topBlock: BlockSvg,
orphanBlock: BlockSvg,
localType: number,
): boolean {
const orphanConnection =
localType === ConnectionType.OUTPUT_VALUE
? orphanBlock.outputConnection
: orphanBlock.previousConnection;
return !!Connection.getConnectionForOrphanedConnection(
topBlock as Block,
orphanConnection as Connection,
);
}

/**
* Returns true if the current candidate is better than the new candidate.
*
* We slightly prefer the current candidate even if it is farther away.
*/
private currCandidateIsBetter(
currCandiate: ConnectionCandidate,
delta: Coordinate,
newCandidate: ConnectionCandidate,
): boolean {
const {local: currLocal, neighbour: currNeighbour} = currCandiate;
const localPos = new Coordinate(currLocal.x, currLocal.y);
const neighbourPos = new Coordinate(currNeighbour.x, currNeighbour.y);
const distance = Coordinate.distance(
Coordinate.sum(localPos, delta),
neighbourPos,
);
return (
newCandidate.distance > distance - config.currentConnectionPreference
);
}

/**
* Returns the closest valid candidate connection, if one can be found.
*
* Valid neighbour connections are within the configured start radius, with a
* compatible type (input, output, etc) and connection check.
*/
private getConnectionCandidate(
draggingBlock: BlockSvg,
delta: Coordinate,
): ConnectionCandidate | null {
const localConns = this.getLocalConnections(draggingBlock);
let radius = config.snapRadius;
let candidate = null;

for (const conn of localConns) {
const {connection: neighbour, radius: rad} = conn.closest(radius, delta);
if (neighbour) {
candidate = {
local: conn,
neighbour: neighbour,
distance: rad,
};
radius = rad;
}
}
this.dragTarget_ && this.dragTarget_.onDragOver(this.draggingBlock_);

return candidate;
}

/**
* Returns all of the connections we might connect to blocks on the workspace.
*
* Includes any connections on the dragging block, and any last next
* connection on the stack (if one exists).
*/
private getLocalConnections(draggingBlock: BlockSvg): RenderedConnection[] {
const available = draggingBlock.getConnections_(false);
const lastOnStack = draggingBlock.lastConnectionInStack(true);
if (lastOnStack && lastOnStack !== draggingBlock.nextConnection) {
available.push(lastOnStack);
}
return available;
}

/**
Expand All @@ -216,6 +381,7 @@ export class BlockDragger implements IBlockDragger {
dom.stopTextWidthCache();

blockAnimation.disconnectUiStop();
this.connectionPreviewer.hidePreview();

const preventMove =
!!this.dragTarget_ &&
Expand Down Expand Up @@ -298,15 +464,33 @@ export class BlockDragger implements IBlockDragger {
*/
protected updateBlockAfterMove_() {
this.fireMoveEvent_();
if (this.draggedConnectionManager_.wouldConnectBlock()) {
if (this.connectionCandidate) {
// Applying connections also rerenders the relevant blocks.
this.draggedConnectionManager_.applyConnections();
this.applyConnections(this.connectionCandidate);
} else {
this.draggingBlock_.queueRender();
}
this.draggingBlock_.scheduleSnapAndBump();
}

private applyConnections(candidate: ConnectionCandidate) {
const {local, neighbour} = candidate;
local.connect(neighbour);
// TODO: We can remove this `rendered` check when we reconcile with v11.
if (this.draggingBlock_.rendered) {
const inferiorConnection = local.isSuperior() ? neighbour : local;
const rootBlock = this.draggingBlock_.getRootBlock();

finishQueuedRenders().then(() => {
blockAnimation.connectionUiEffect(inferiorConnection.getSourceBlock());
// bringToFront is incredibly expensive. Delay until the next frame.
setTimeout(() => {
rootBlock.bringToFront();
}, 0);
});
}
}

/** Fire a UI event at the end of a block drag. */
protected fireDragEndEvent_() {
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
Expand Down Expand Up @@ -415,14 +599,9 @@ export class BlockDragger implements IBlockDragger {
* @returns A possibly empty list of insertion marker blocks.
*/
getInsertionMarkers(): BlockSvg[] {
// No insertion markers with the old style of dragged connection managers.
if (
this.draggedConnectionManager_ &&
this.draggedConnectionManager_.getInsertionMarkers
) {
return this.draggedConnectionManager_.getInsertionMarkers();
}
return [];
return this.workspace_
.getAllBlocks()
.filter((block) => block.isInsertionMarker());
}
}

Expand Down
9 changes: 7 additions & 2 deletions core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1722,7 +1722,8 @@ export class BlockSvg
* @internal
*/
fadeForReplacement(add: boolean) {
this.pathObject.updateReplacementFade(add);
// TODO (7204): Remove these internal methods.
(this.pathObject as AnyDuringMigration).updateReplacementFade(add);
}

/**
Expand All @@ -1734,6 +1735,10 @@ export class BlockSvg
* @internal
*/
highlightShapeForInput(conn: RenderedConnection, add: boolean) {
this.pathObject.updateShapeForInputHighlight(conn, add);
// TODO (7204): Remove these internal methods.
(this.pathObject as AnyDuringMigration).updateShapeForInputHighlight(
conn,
add,
);
}
}
Loading
Loading