-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Expand file tree
/
Copy pathblock_paster.ts
More file actions
154 lines (138 loc) · 4.62 KB
/
block_paster.ts
File metadata and controls
154 lines (138 loc) · 4.62 KB
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
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {BlockSvg} from '../block_svg.js';
import {IFocusableNode} from '../blockly.js';
import {config} from '../config.js';
import {EventType} from '../events/type.js';
import * as eventUtils from '../events/utils.js';
import {getFocusManager} from '../focus_manager.js';
import {ICopyData} from '../interfaces/i_copyable.js';
import {IPaster} from '../interfaces/i_paster.js';
import * as renderManagement from '../render_management.js';
import {State, append} from '../serialization/blocks.js';
import {Coordinate} from '../utils/coordinate.js';
import {WorkspaceSvg} from '../workspace_svg.js';
import * as registry from './registry.js';
export class BlockPaster implements IPaster<BlockCopyData, BlockSvg> {
static TYPE = 'block';
paste(
copyData: BlockCopyData,
workspace: WorkspaceSvg,
coordinate?: Coordinate,
): BlockSvg | null {
if (!workspace.isCapacityAvailable(copyData.typeCounts!)) return null;
if (coordinate) {
copyData.blockState['x'] = coordinate.x;
copyData.blockState['y'] = coordinate.y;
}
// After appending the block to the workspace, it will be bumped from its neighbors
// However, the algorithm for deciding where to paste a block depends on
// the starting position of the copied block, so we'll pass those coordinates along
const initialCoordinates =
coordinate ||
new Coordinate(
copyData.blockState['x'] || 0,
copyData.blockState['y'] || 0,
);
eventUtils.disable();
let block;
try {
block = append(copyData.blockState, workspace) as BlockSvg;
moveBlockToNotConflict(block, initialCoordinates);
} finally {
eventUtils.enable();
}
if (!block) return block;
if (eventUtils.isEnabled() && !block.isShadow()) {
eventUtils.fire(new (eventUtils.get(EventType.BLOCK_CREATE))(block));
}
// Sometimes there's a delay before the block is fully created and ready for
// focusing, so wait slightly before focusing the newly pasted block.
const nodeToFocus: IFocusableNode = block;
renderManagement
.finishQueuedRenders()
.then(() => getFocusManager().focusNode(nodeToFocus));
return block;
}
}
/**
* Moves the given block to a location where it does not: (1) overlap exactly
* with any other blocks, or (2) look like it is connected to any other blocks.
*
* Exported for testing.
*
* @param block The block to move to an unambiguous location.
* @param originalPosition The initial coordinate to start searching from,
* likely the position of the copied block.
* @internal
*/
export function moveBlockToNotConflict(
block: BlockSvg,
originalPosition: Coordinate,
) {
if (block.workspace.RTL) {
originalPosition.x = block.workspace.getWidth() - originalPosition.x;
}
const workspace = block.workspace;
const snapRadius = config.snapRadius;
const bumpOffset = Coordinate.difference(
originalPosition,
block.getRelativeToSurfaceXY(),
);
const offset = new Coordinate(0, 0);
// getRelativeToSurfaceXY is really expensive, so we want to cache this.
const otherCoords = workspace
.getAllBlocks(false)
.filter((otherBlock) => otherBlock.id != block.id)
.map((b) => b.getRelativeToSurfaceXY());
while (
blockOverlapsOtherExactly(
Coordinate.sum(originalPosition, offset),
otherCoords,
) ||
blockIsInSnapRadius(block, Coordinate.sum(bumpOffset, offset), snapRadius)
) {
if (workspace.RTL) {
offset.translate(-snapRadius, snapRadius * 2);
} else {
offset.translate(snapRadius, snapRadius * 2);
}
}
block!.moveTo(Coordinate.sum(originalPosition, offset));
}
/**
* @returns true if the given block coordinates are less than a delta of 1 from
* any of the other coordinates.
*/
function blockOverlapsOtherExactly(
coord: Coordinate,
otherCoords: Coordinate[],
): boolean {
return otherCoords.some(
(otherCoord) =>
Math.abs(otherCoord.x - coord.x) <= 1 &&
Math.abs(otherCoord.y - coord.y) <= 1,
);
}
/**
* @returns true if the given block (when offset by the given amount) is close
* enough to any other connections (within the snap radius) that it looks
* like they could connect.
*/
function blockIsInSnapRadius(
block: BlockSvg,
offset: Coordinate,
snapRadius: number,
): boolean {
return block
.getConnections_(false)
.some((connection) => !!connection.closest(snapRadius, offset).connection);
}
export interface BlockCopyData extends ICopyData {
blockState: State;
typeCounts: {[key: string]: number};
}
registry.register(BlockPaster.TYPE, new BlockPaster());