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

fix: bumping copied objects #7349

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 83 additions & 3 deletions core/clipboard/block_paster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {IPaster} from '../interfaces/i_paster.js';
import {State, append} from '../serialization/blocks.js';
import {Coordinate} from '../utils/coordinate.js';
import {WorkspaceSvg} from '../workspace_svg.js';
import * as eventUtils from '../events/utils.js';
import {config} from '../config.js';

export class BlockPaster implements IPaster<BlockCopyData, BlockSvg> {
static TYPE = 'block';
Expand All @@ -26,12 +28,90 @@ export class BlockPaster implements IPaster<BlockCopyData, BlockSvg> {
copyData.blockState['x'] = coordinate.x;
copyData.blockState['y'] = coordinate.y;
}
return append(copyData.blockState, workspace, {
recordUndo: true,
}) as BlockSvg;

eventUtils.disable();
let block;
try {
block = append(copyData.blockState, workspace) as BlockSvg;
moveBlockToNotConflict(block);
} finally {
eventUtils.enable();
}

if (!block) return block;

if (eventUtils.isEnabled() && !block.isShadow()) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block));
}
block.select();
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.
* @internal
*/
export function moveBlockToNotConflict(block: BlockSvg) {
const workspace = block.workspace;
const snapRadius = config.snapRadius;
const coord = 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(coord, offset), otherCoords) ||
blockIsInSnapRadius(block, offset, snapRadius)
) {
if (workspace.RTL) {
offset.translate(-snapRadius, snapRadius * 2);
} else {
offset.translate(snapRadius, snapRadius * 2);
}
}

block!.moveTo(Coordinate.sum(coord, 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};
Expand Down
10 changes: 8 additions & 2 deletions core/clipboard/workspace_comment_paster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ export class WorkspaceCommentPaster
workspace: WorkspaceSvg,
coordinate?: Coordinate,
): WorkspaceCommentSvg {
const state = copyData.commentState;
if (coordinate) {
copyData.commentState.setAttribute('x', `${coordinate.x}`);
copyData.commentState.setAttribute('y', `${coordinate.y}`);
state.setAttribute('x', `${coordinate.x}`);
state.setAttribute('y', `${coordinate.y}`);
} else {
const x = parseInt(state.getAttribute('x') ?? '0') + 50;
const y = parseInt(state.getAttribute('y') ?? '0') + 50;
state.setAttribute('x', `${x}`);
state.setAttribute('y', `${y}`);
}
return WorkspaceCommentSvg.fromXmlRendered(
copyData.commentState,
Expand Down
4 changes: 3 additions & 1 deletion core/serialization/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,9 @@ export function appendInternal(
const block = appendPrivate(state, workspace, {parentConnection, isShadow});

eventUtils.enable();
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block));
if (eventUtils.isEnabled()) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block));
}
eventUtils.setGroup(existingGroup);
eventUtils.setRecordUndo(prevRecordUndo);

Expand Down
7 changes: 7 additions & 0 deletions core/workspace_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1388,6 +1388,10 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
for (let i = 0, connection; (connection = connections[i]); i++) {
const neighbour = connection.closest(
config.snapRadius,
// TODO: This code doesn't work because it's passing an absolute
// coordinate instead of a relative coordinate. Need to
// figure out if I'm deprecating this function or if I
// need to fix this.
new Coordinate(blockX, blockY),
);
if (neighbour.connection) {
Expand Down Expand Up @@ -1441,6 +1445,9 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
// with any blocks.
commentX += 50;
commentY += 50;
// TODO: This code doesn't work because it's using absolute coords
// where relative coords are expected. Need to figure out what I'm
// doing with this function and if I need to fix it.
comment.moveBy(commentX, commentY);
}
} finally {
Expand Down
105 changes: 103 additions & 2 deletions tests/mocha/clipboard_test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2019 Google LLC
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

Expand All @@ -10,11 +10,15 @@ import {
sharedTestSetup,
sharedTestTeardown,
} from './test_helpers/setup_teardown.js';
import {
assertEventFired,
createChangeListenerSpy,
} from './test_helpers/events.js';

suite('Clipboard', function () {
setup(function () {
this.clock = sharedTestSetup.call(this, {fireEventsNow: false}).clock;
this.workspace = new Blockly.WorkspaceSvg(new Blockly.Options({}));
this.workspace = Blockly.inject('blocklyDiv');
});

teardown(function () {
Expand All @@ -32,4 +36,101 @@ suite('Clipboard', function () {

Blockly.clipboard.registry.unregister('test-paster');
});

suite('pasting blocks', function () {
test('pasting blocks fires a create event', function () {
const eventSpy = createChangeListenerSpy(this.workspace);
const block = Blockly.serialization.blocks.append(
{
'type': 'controls_if',
'id': 'blockId',
},
this.workspace,
);
const data = block.toCopyData();
this.clock.runAll();
eventSpy.resetHistory();

Blockly.clipboard.paste(data, this.workspace);
this.clock.runAll();

assertEventFired(
eventSpy,
Blockly.Events.BlockCreate,
{'recordUndo': true, 'type': Blockly.Events.BLOCK_CREATE},
this.workspace.id,
);
});

suite('pasted blocks are placed in unambiguous locations', function () {
test('pasted blocks are bumped to not overlap', function () {
const block = Blockly.serialization.blocks.append(
{
'type': 'controls_if',
'x': 38,
'y': 13,
},
this.workspace,
);
const data = block.toCopyData();

const newBlock = Blockly.clipboard.paste(data, this.workspace);
chai.assert.deepEqual(
newBlock.getRelativeToSurfaceXY(),
new Blockly.utils.Coordinate(66, 69),
);
});

test('pasted blocks are bumped to be outside the connection snap radius', function () {
Blockly.serialization.workspaces.load(
{
'blocks': {
'languageVersion': 0,
'blocks': [
{
'type': 'controls_if',
'id': 'sourceBlockId',
'x': 38,
'y': 13,
},
{
'type': 'logic_compare',
'x': 113,
'y': 63,
},
],
},
},
this.workspace,
);
this.clock.runAll(); // Update the connection DB.
const data = this.workspace.getBlockById('sourceBlockId').toCopyData();

const newBlock = Blockly.clipboard.paste(data, this.workspace);
chai.assert.deepEqual(
newBlock.getRelativeToSurfaceXY(),
new Blockly.utils.Coordinate(94, 125),
);
});
});
});

suite('pasting comments', function () {
test('pasted comments are bumped to not overlap', function () {
Blockly.Xml.domToWorkspace(
Blockly.utils.xml.textToDom(
'<xml><comment id="test" x=10 y=10/></xml>',
),
this.workspace,
);
const comment = this.workspace.getTopComments(false)[0];
const data = comment.toCopyData();

const newComment = Blockly.clipboard.paste(data, this.workspace);
chai.assert.deepEqual(
newComment.getRelativeToSurfaceXY(),
new Blockly.utils.Coordinate(60, 60),
);
});
});
});
11 changes: 4 additions & 7 deletions tests/mocha/test_helpers/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,10 @@ export function assertEventFired(
expectedWorkspaceId,
expectedBlockId,
) {
expectedProperties = Object.assign(
{
workspaceId: expectedWorkspaceId,
blockId: expectedBlockId,
},
expectedProperties,
);
const baseProps = {};
if (expectedWorkspaceId) baseProps.workspaceId = expectedWorkspaceId;
if (expectedBlockId) baseProps.blockId = expectedBlockId;
expectedProperties = Object.assign(baseProps, expectedProperties);
const expectedEvent = sinon.match
.instanceOf(instanceType)
.and(sinon.match(expectedProperties));
Expand Down