Skip to content

Commit 592c541

Browse files
authored
release: v10.4.0
Merge pull request #7858 from google/rc/v10.4.0
2 parents 1ba0e55 + 1126906 commit 592c541

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

115 files changed

+2216
-719
lines changed

.github/workflows/appengine_deploy.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
npm run prepareDemos
2525
2626
- name: Upload
27-
uses: actions/upload-artifact@v3
27+
uses: actions/upload-artifact@v4
2828
with:
2929
name: appengine_files
3030
path: _deploy/
@@ -36,13 +36,13 @@ jobs:
3636
needs: prepare
3737
steps:
3838
- name: Download prepared files
39-
uses: actions/download-artifact@v3
39+
uses: actions/download-artifact@v4
4040
with:
4141
name: appengine_files
4242
path: _deploy/
4343

4444
- name: Deploy to App Engine
45-
uses: google-github-actions/deploy-appengine@v1.2.7
45+
uses: google-github-actions/deploy-appengine@v2.0.0
4646
# For parameters see:
4747
# https://github.com/google-github-actions/deploy-appengine#inputs
4848
with:

appengine/app.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
runtime: python37
1+
runtime: python312
22

33
handlers:
44
# Redirect obsolete URLs.

core/block_dragger.ts

Lines changed: 219 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import * as common from './common.js';
2121
import type {BlockMove} from './events/events_block_move.js';
2222
import * as eventUtils from './events/utils.js';
2323
import type {Icon} from './icons/icon.js';
24-
import {InsertionMarkerManager} from './insertion_marker_manager.js';
2524
import type {IBlockDragger} from './interfaces/i_block_dragger.js';
2625
import type {IDragTarget} from './interfaces/i_drag_target.js';
2726
import * as registry from './registry.js';
@@ -31,6 +30,26 @@ import type {WorkspaceSvg} from './workspace_svg.js';
3130
import {hasBubble} from './interfaces/i_has_bubble.js';
3231
import * as deprecation from './utils/deprecation.js';
3332
import * as layers from './layers.js';
33+
import {ConnectionType, IConnectionPreviewer} from './blockly.js';
34+
import {RenderedConnection} from './rendered_connection.js';
35+
import {config} from './config.js';
36+
import {ComponentManager} from './component_manager.js';
37+
import {IDeleteArea} from './interfaces/i_delete_area.js';
38+
import {Connection} from './connection.js';
39+
import {Block} from './block.js';
40+
import {finishQueuedRenders} from './render_management.js';
41+
42+
/** Represents a nearby valid connection. */
43+
interface ConnectionCandidate {
44+
/** A connection on the dragging stack that is compatible with neighbour. */
45+
local: RenderedConnection;
46+
47+
/** A nearby connection that is compatible with local. */
48+
neighbour: RenderedConnection;
49+
50+
/** The distance between the local connection and the neighbour connection. */
51+
distance: number;
52+
}
3453

3554
/**
3655
* Class for a block dragger. It moves blocks around the workspace when they
@@ -39,14 +58,17 @@ import * as layers from './layers.js';
3958
export class BlockDragger implements IBlockDragger {
4059
/** The top block in the stack that is being dragged. */
4160
protected draggingBlock_: BlockSvg;
42-
protected draggedConnectionManager_: InsertionMarkerManager;
61+
62+
protected connectionPreviewer: IConnectionPreviewer;
4363

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

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

70+
private connectionCandidate: ConnectionCandidate | null = null;
71+
5072
/** Whether the block would be deleted if dropped immediately. */
5173
protected wouldDeleteBlock_ = false;
5274
protected startXY_: Coordinate;
@@ -63,13 +85,13 @@ export class BlockDragger implements IBlockDragger {
6385
*/
6486
constructor(block: BlockSvg, workspace: WorkspaceSvg) {
6587
this.draggingBlock_ = block;
88+
this.workspace_ = workspace;
6689

67-
/** Object that keeps track of connections on dragged blocks. */
68-
this.draggedConnectionManager_ = new InsertionMarkerManager(
69-
this.draggingBlock_,
90+
const previewerConstructor = registry.getClassFromOptions(
91+
registry.Type.CONNECTION_PREVIEWER,
92+
this.workspace_.options,
7093
);
71-
72-
this.workspace_ = workspace;
94+
this.connectionPreviewer = new previewerConstructor!(block);
7395

7496
/**
7597
* The location of the top left corner of the dragging block at the
@@ -87,9 +109,7 @@ export class BlockDragger implements IBlockDragger {
87109
*/
88110
dispose() {
89111
this.dragIconData_.length = 0;
90-
if (this.draggedConnectionManager_) {
91-
this.draggedConnectionManager_.dispose();
92-
}
112+
this.connectionPreviewer.dispose();
93113
}
94114

95115
/**
@@ -155,7 +175,6 @@ export class BlockDragger implements IBlockDragger {
155175

156176
this.draggingBlock_.translate(newLoc.x, newLoc.y);
157177
blockAnimation.disconnectUiEffect(this.draggingBlock_);
158-
this.draggedConnectionManager_.updateAvailableConnections();
159178
}
160179

161180
/** Fire a UI event at the start of a block drag. */
@@ -173,32 +192,178 @@ export class BlockDragger implements IBlockDragger {
173192
* display accordingly.
174193
*
175194
* @param e The most recent move event.
176-
* @param currentDragDeltaXY How far the pointer has moved from the position
195+
* @param delta How far the pointer has moved from the position
177196
* at the start of the drag, in pixel units.
178197
*/
179-
drag(e: PointerEvent, currentDragDeltaXY: Coordinate) {
180-
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
198+
drag(e: PointerEvent, delta: Coordinate) {
199+
const block = this.draggingBlock_;
200+
this.moveBlock(block, delta);
201+
this.updateDragTargets(e, block);
202+
this.wouldDeleteBlock_ = this.wouldDeleteBlock(e, block, delta);
203+
this.updateCursorDuringBlockDrag_();
204+
this.updateConnectionPreview(block, delta);
205+
}
206+
207+
private moveBlock(draggingBlock: BlockSvg, dragDelta: Coordinate) {
208+
const delta = this.pixelsToWorkspaceUnits_(dragDelta);
181209
const newLoc = Coordinate.sum(this.startXY_, delta);
182-
this.draggingBlock_.moveDuringDrag(newLoc);
210+
draggingBlock.moveDuringDrag(newLoc);
211+
}
212+
213+
private updateDragTargets(e: PointerEvent, draggingBlock: BlockSvg) {
214+
const newDragTarget = this.workspace_.getDragTarget(e);
215+
if (this.dragTarget_ !== newDragTarget) {
216+
this.dragTarget_?.onDragExit(draggingBlock);
217+
newDragTarget?.onDragEnter(draggingBlock);
218+
}
219+
newDragTarget?.onDragOver(draggingBlock);
220+
this.dragTarget_ = newDragTarget;
221+
}
183222

184-
const oldDragTarget = this.dragTarget_;
185-
this.dragTarget_ = this.workspace_.getDragTarget(e);
223+
/**
224+
* Returns true if we would delete the block if it was dropped at this time,
225+
* false otherwise.
226+
*/
227+
private wouldDeleteBlock(
228+
e: PointerEvent,
229+
draggingBlock: BlockSvg,
230+
delta: Coordinate,
231+
): boolean {
232+
const dragTarget = this.workspace_.getDragTarget(e);
233+
if (!dragTarget) return false;
234+
235+
const componentManager = this.workspace_.getComponentManager();
236+
const isDeleteArea = componentManager.hasCapability(
237+
dragTarget.id,
238+
ComponentManager.Capability.DELETE_AREA,
239+
);
240+
if (!isDeleteArea) return false;
241+
242+
return (dragTarget as IDeleteArea).wouldDelete(
243+
draggingBlock,
244+
!!this.getConnectionCandidate(draggingBlock, delta),
245+
);
246+
}
186247

187-
this.draggedConnectionManager_.update(delta, this.dragTarget_);
188-
const oldWouldDeleteBlock = this.wouldDeleteBlock_;
189-
this.wouldDeleteBlock_ = this.draggedConnectionManager_.wouldDeleteBlock;
190-
if (oldWouldDeleteBlock !== this.wouldDeleteBlock_) {
191-
// Prevent unnecessary add/remove class calls.
192-
this.updateCursorDuringBlockDrag_();
248+
private updateConnectionPreview(draggingBlock: BlockSvg, delta: Coordinate) {
249+
const currCandidate = this.connectionCandidate;
250+
const newCandidate = this.getConnectionCandidate(draggingBlock, delta);
251+
if (!newCandidate) {
252+
this.connectionPreviewer.hidePreview();
253+
this.connectionCandidate = null;
254+
return;
255+
}
256+
const candidate =
257+
currCandidate &&
258+
this.currCandidateIsBetter(currCandidate, delta, newCandidate)
259+
? currCandidate
260+
: newCandidate;
261+
this.connectionCandidate = candidate;
262+
const {local, neighbour} = candidate;
263+
if (
264+
(local.type === ConnectionType.OUTPUT_VALUE ||
265+
local.type === ConnectionType.PREVIOUS_STATEMENT) &&
266+
neighbour.isConnected() &&
267+
!neighbour.targetBlock()!.isInsertionMarker() &&
268+
!this.orphanCanConnectAtEnd(
269+
draggingBlock,
270+
neighbour.targetBlock()!,
271+
local.type,
272+
)
273+
) {
274+
this.connectionPreviewer.previewReplacement(
275+
local,
276+
neighbour,
277+
neighbour.targetBlock()!,
278+
);
279+
return;
193280
}
281+
this.connectionPreviewer.previewConnection(local, neighbour);
282+
}
194283

195-
// Call drag enter/exit/over after wouldDeleteBlock is called in
196-
// InsertionMarkerManager.update.
197-
if (this.dragTarget_ !== oldDragTarget) {
198-
oldDragTarget && oldDragTarget.onDragExit(this.draggingBlock_);
199-
this.dragTarget_ && this.dragTarget_.onDragEnter(this.draggingBlock_);
284+
/**
285+
* Returns true if the given orphan block can connect at the end of the
286+
* top block's stack or row, false otherwise.
287+
*/
288+
private orphanCanConnectAtEnd(
289+
topBlock: BlockSvg,
290+
orphanBlock: BlockSvg,
291+
localType: number,
292+
): boolean {
293+
const orphanConnection =
294+
localType === ConnectionType.OUTPUT_VALUE
295+
? orphanBlock.outputConnection
296+
: orphanBlock.previousConnection;
297+
return !!Connection.getConnectionForOrphanedConnection(
298+
topBlock as Block,
299+
orphanConnection as Connection,
300+
);
301+
}
302+
303+
/**
304+
* Returns true if the current candidate is better than the new candidate.
305+
*
306+
* We slightly prefer the current candidate even if it is farther away.
307+
*/
308+
private currCandidateIsBetter(
309+
currCandiate: ConnectionCandidate,
310+
delta: Coordinate,
311+
newCandidate: ConnectionCandidate,
312+
): boolean {
313+
const {local: currLocal, neighbour: currNeighbour} = currCandiate;
314+
const localPos = new Coordinate(currLocal.x, currLocal.y);
315+
const neighbourPos = new Coordinate(currNeighbour.x, currNeighbour.y);
316+
const distance = Coordinate.distance(
317+
Coordinate.sum(localPos, delta),
318+
neighbourPos,
319+
);
320+
return (
321+
newCandidate.distance > distance - config.currentConnectionPreference
322+
);
323+
}
324+
325+
/**
326+
* Returns the closest valid candidate connection, if one can be found.
327+
*
328+
* Valid neighbour connections are within the configured start radius, with a
329+
* compatible type (input, output, etc) and connection check.
330+
*/
331+
private getConnectionCandidate(
332+
draggingBlock: BlockSvg,
333+
delta: Coordinate,
334+
): ConnectionCandidate | null {
335+
const localConns = this.getLocalConnections(draggingBlock);
336+
let radius = config.snapRadius;
337+
let candidate = null;
338+
339+
for (const conn of localConns) {
340+
const {connection: neighbour, radius: rad} = conn.closest(radius, delta);
341+
if (neighbour) {
342+
candidate = {
343+
local: conn,
344+
neighbour: neighbour,
345+
distance: rad,
346+
};
347+
radius = rad;
348+
}
200349
}
201-
this.dragTarget_ && this.dragTarget_.onDragOver(this.draggingBlock_);
350+
351+
return candidate;
352+
}
353+
354+
/**
355+
* Returns all of the connections we might connect to blocks on the workspace.
356+
*
357+
* Includes any connections on the dragging block, and any last next
358+
* connection on the stack (if one exists).
359+
*/
360+
private getLocalConnections(draggingBlock: BlockSvg): RenderedConnection[] {
361+
const available = draggingBlock.getConnections_(false);
362+
const lastOnStack = draggingBlock.lastConnectionInStack(true);
363+
if (lastOnStack && lastOnStack !== draggingBlock.nextConnection) {
364+
available.push(lastOnStack);
365+
}
366+
return available;
202367
}
203368

204369
/**
@@ -216,6 +381,8 @@ export class BlockDragger implements IBlockDragger {
216381
dom.stopTextWidthCache();
217382

218383
blockAnimation.disconnectUiStop();
384+
this.connectionPreviewer.hidePreview();
385+
this.connectionPreviewer.dispose();
219386

220387
const preventMove =
221388
!!this.dragTarget_ &&
@@ -298,15 +465,33 @@ export class BlockDragger implements IBlockDragger {
298465
*/
299466
protected updateBlockAfterMove_() {
300467
this.fireMoveEvent_();
301-
if (this.draggedConnectionManager_.wouldConnectBlock()) {
468+
if (this.connectionCandidate) {
302469
// Applying connections also rerenders the relevant blocks.
303-
this.draggedConnectionManager_.applyConnections();
470+
this.applyConnections(this.connectionCandidate);
304471
} else {
305472
this.draggingBlock_.queueRender();
306473
}
307474
this.draggingBlock_.scheduleSnapAndBump();
308475
}
309476

477+
private applyConnections(candidate: ConnectionCandidate) {
478+
const {local, neighbour} = candidate;
479+
local.connect(neighbour);
480+
// TODO: We can remove this `rendered` check when we reconcile with v11.
481+
if (this.draggingBlock_.rendered) {
482+
const inferiorConnection = local.isSuperior() ? neighbour : local;
483+
const rootBlock = this.draggingBlock_.getRootBlock();
484+
485+
finishQueuedRenders().then(() => {
486+
blockAnimation.connectionUiEffect(inferiorConnection.getSourceBlock());
487+
// bringToFront is incredibly expensive. Delay until the next frame.
488+
setTimeout(() => {
489+
rootBlock.bringToFront();
490+
}, 0);
491+
});
492+
}
493+
}
494+
310495
/** Fire a UI event at the end of a block drag. */
311496
protected fireDragEndEvent_() {
312497
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
@@ -415,14 +600,9 @@ export class BlockDragger implements IBlockDragger {
415600
* @returns A possibly empty list of insertion marker blocks.
416601
*/
417602
getInsertionMarkers(): BlockSvg[] {
418-
// No insertion markers with the old style of dragged connection managers.
419-
if (
420-
this.draggedConnectionManager_ &&
421-
this.draggedConnectionManager_.getInsertionMarkers
422-
) {
423-
return this.draggedConnectionManager_.getInsertionMarkers();
424-
}
425-
return [];
603+
return this.workspace_
604+
.getAllBlocks()
605+
.filter((block) => block.isInsertionMarker());
426606
}
427607
}
428608

0 commit comments

Comments
 (0)