Skip to content

Commit 9f4f1c1

Browse files
authored
fix: delete-area hit testing at non-default zoom (#10095) (#10096)
* chore: sync package.json * fix: delete-area hit testing at non-default zoom (#10095) * chore: revert moving recordDragTargets call * Revert "chore: sync package.json" This reverts commit 3366b79.
1 parent d04bc19 commit 9f4f1c1

3 files changed

Lines changed: 184 additions & 50 deletions

File tree

packages/blockly/core/dragging/dragger.ts

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import type {IDragger} from '../interfaces/i_dragger.js';
1616
import {isFocusableNode} from '../interfaces/i_focusable_node.js';
1717
import * as registry from '../registry.js';
1818
import {Coordinate} from '../utils/coordinate.js';
19-
import {screenToWsCoordinates} from '../utils/svg_math.js';
2019

2120
export class Dragger implements IDragger {
2221
protected startLoc: Coordinate;
@@ -50,7 +49,10 @@ export class Dragger implements IDragger {
5049
const pointerEvent = e instanceof PointerEvent ? e : null;
5150
if (!pointerEvent) return;
5251

53-
const coordinate = this.pointerToWorkspaceCoordinate(pointerEvent);
52+
const coordinate = new Coordinate(
53+
pointerEvent.clientX,
54+
pointerEvent.clientY,
55+
);
5456
// Must check `wouldDelete` before calling other hooks on drag targets
5557
// since we have documented that we would do so.
5658
if (isDeletable(this.draggable)) {
@@ -122,7 +124,10 @@ export class Dragger implements IDragger {
122124
return;
123125
}
124126

125-
const coordinate = this.pointerToWorkspaceCoordinate(pointerEvent);
127+
const coordinate = new Coordinate(
128+
pointerEvent.clientX,
129+
pointerEvent.clientY,
130+
);
126131
const dragTarget = this.draggable.workspace.getDragTarget(coordinate);
127132

128133
if (dragTarget) {
@@ -182,17 +187,6 @@ export class Dragger implements IDragger {
182187
return dragTarget.shouldPreventMove(rootDraggable);
183188
}
184189

185-
/**
186-
* Returns the workspace coordinate for a pointer position, for delete-area
187-
* hit testing.
188-
*/
189-
private pointerToWorkspaceCoordinate(e: PointerEvent): Coordinate {
190-
return screenToWsCoordinates(
191-
this.draggable.workspace,
192-
new Coordinate(e.clientX, e.clientY),
193-
);
194-
}
195-
196190
protected pixelsToWorkspaceUnits(pixelCoord: Coordinate): Coordinate {
197191
const result = new Coordinate(
198192
pixelCoord.x / this.draggable.workspace.scale,

packages/blockly/core/workspace_svg.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1517,17 +1517,16 @@ export class WorkspaceSvg
15171517
/* eslint-enable */
15181518

15191519
/**
1520-
* Returns the drag target the pointer event is over.
1520+
* Returns the drag target at the given point.
15211521
*
1522-
* @param e Pointer move event or a workspace coordinate.
1523-
* @returns Null if not over a drag target, or the drag target the event is
1524-
* over.
1522+
* @param point A pointer event, or a point in client/viewport coordinates.
1523+
* @returns Null if not over a drag target, or the drag target at that point.
15251524
*/
1526-
getDragTarget(e: PointerEvent | Coordinate): IDragTarget | null {
1525+
getDragTarget(point: PointerEvent | Coordinate): IDragTarget | null {
15271526
const coordinate =
1528-
e instanceof Coordinate
1529-
? svgMath.wsToScreenCoordinates(this, e)
1530-
: new Coordinate(e.clientX, e.clientY);
1527+
point instanceof Coordinate
1528+
? point
1529+
: new Coordinate(point.clientX, point.clientY);
15311530
for (let i = 0, targetArea; (targetArea = this.dragTargetAreas[i]); i++) {
15321531
if (targetArea.clientRect.contains(coordinate.x, coordinate.y)) {
15331532
return targetArea.component;

packages/blockly/tests/mocha/dragger_test.js

Lines changed: 169 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@ suite('Dragger', function () {
3232
* @returns {{x: number, y: number}} Viewport coordinates at the block origin.
3333
*/
3434
function blockOriginClient(block) {
35-
const screenCoords = Blockly.utils.svgMath.wsToScreenCoordinates(
36-
block.workspace,
37-
block.getRelativeToSurfaceXY(),
38-
);
35+
const ws = block.workspace;
36+
let point = block.getRelativeToSurfaceXY();
37+
if (ws.isMutator) {
38+
point = point.scale(ws.options.parentWorkspace.scale);
39+
}
40+
const screenCoords = Blockly.utils.svgMath.wsToScreenCoordinates(ws, point);
3941
return {x: screenCoords.x, y: screenCoords.y};
4042
}
4143

@@ -64,13 +66,29 @@ suite('Dragger', function () {
6466
return block.getSvgRoot().classList.contains('blocklyDraggingDelete');
6567
}
6668

69+
/**
70+
* @param {!Blockly.WorkspaceSvg} workspace The workspace with a trashcan.
71+
* @returns {boolean} Whether the trashcan lid open style is applied.
72+
*/
73+
function hasTrashLidOpen(workspace) {
74+
return workspace.trashcan.svgGroup.classList.contains('blocklyTrashOpen');
75+
}
76+
77+
/**
78+
* @param {!Blockly.WorkspaceSvg} workspace The workspace to zoom.
79+
* @param {number} scale The target zoom factor.
80+
*/
81+
function setWorkspaceScale(workspace, scale) {
82+
workspace.setScale(scale);
83+
}
84+
6785
/**
6886
* Simulates pressing on the block center and dragging to a viewport point.
6987
*
7088
* @param {!Blockly.BlockSvg} block The block to drag.
7189
* @param {{x: number, y: number}} pointerEnd The viewport point to drag to.
72-
* @returns {{dragger: !Blockly.dragging.Dragger, dragEvent: !PointerEvent}}
73-
* The dragger and final pointer event from the simulated drag.
90+
* @returns {{dragger: !Blockly.dragging.Dragger, dragEvent: !PointerEvent, block: !Blockly.BlockSvg}}
91+
* The dragger, final pointer event, and block being dragged.
7492
*/
7593
function dragBlock(block, pointerEnd) {
7694
const start = blockCenterClient(block);
@@ -86,7 +104,7 @@ suite('Dragger', function () {
86104
dragger.onDragStart(dragStartEvent);
87105
dragger.onDrag(dragEvent, totalDelta);
88106

89-
return {dragger, dragEvent};
107+
return {dragger, dragEvent, block: dragger.draggable};
90108
}
91109

92110
setup(function () {
@@ -110,25 +128,45 @@ suite('Dragger', function () {
110128
sharedTestTeardown.call(this);
111129
});
112130

113-
[
114-
{name: 'trashcan', rectKey: 'trashRect'},
115-
{name: 'toolbox', rectKey: 'toolboxRect'},
116-
].forEach(({name, rectKey}) => {
117-
test(`applies delete styling and deletes when dragged to ${name}`, function () {
118-
const deleteRect = this[rectKey];
119-
const {dragger, dragEvent} = dragBlock(
120-
this.block,
121-
rectCenterClient(deleteRect),
122-
);
131+
const zoomLevels = [
132+
{name: 'default scale', scale: null},
133+
{name: 'zoomed in', scale: 1.5},
134+
{name: 'zoomed out', scale: 0.7},
135+
];
123136

124-
assert.isTrue(
125-
deleteRect.contains(dragEvent.clientX, dragEvent.clientY),
126-
`Expected cursor to be inside ${name} delete area`,
127-
);
128-
assert.isTrue(hasDeleteStyle(this.block));
137+
zoomLevels.forEach(({name: zoomName, scale}) => {
138+
[
139+
{name: 'trashcan', rectKey: 'trashRect', checkLid: true},
140+
{name: 'toolbox', rectKey: 'toolboxRect', checkLid: false},
141+
].forEach(({name, rectKey, checkLid}) => {
142+
test(`applies delete styling and deletes when dragged to ${name} at ${zoomName}`, function () {
143+
if (scale !== null) {
144+
setWorkspaceScale(this.workspace, scale);
145+
this.trashRect = this.workspace.trashcan.getClientRect();
146+
this.toolboxRect = this.workspace.toolbox.getClientRect();
147+
}
129148

130-
dragger.onDragEnd(dragEvent);
131-
assert.isTrue(this.block.isDeadOrDying());
149+
const deleteRect = this[rectKey];
150+
const {dragger, dragEvent, block} = dragBlock(
151+
this.block,
152+
rectCenterClient(deleteRect),
153+
);
154+
155+
assert.isTrue(
156+
deleteRect.contains(dragEvent.clientX, dragEvent.clientY),
157+
`Expected cursor to be inside ${name} delete area`,
158+
);
159+
assert.isTrue(hasDeleteStyle(block));
160+
if (checkLid) {
161+
assert.isTrue(
162+
hasTrashLidOpen(this.workspace),
163+
'Expected trashcan lid to be open',
164+
);
165+
}
166+
167+
dragger.onDragEnd(dragEvent);
168+
assert.isTrue(block.isDeadOrDying());
169+
});
132170
});
133171
});
134172

@@ -140,12 +178,12 @@ suite('Dragger', function () {
140178
x: deleteAreaRect.right - 5,
141179
y: originBefore.y,
142180
};
143-
const {dragger, dragEvent} = dragBlock(this.block, {
181+
const {dragger, dragEvent, block} = dragBlock(this.block, {
144182
x: start.x + desiredOrigin.x - originBefore.x,
145183
y: start.y + desiredOrigin.y - originBefore.y,
146184
});
147185

148-
const originAfter = blockOriginClient(this.block);
186+
const originAfter = blockOriginClient(block);
149187
assert.isTrue(
150188
deleteAreaRect.contains(originAfter.x, originAfter.y),
151189
'Expected block origin to overlap delete area',
@@ -154,9 +192,112 @@ suite('Dragger', function () {
154192
deleteAreaRect.contains(dragEvent.clientX, dragEvent.clientY),
155193
'Expected cursor to be outside delete area',
156194
);
157-
assert.isFalse(hasDeleteStyle(this.block));
195+
assert.isFalse(hasDeleteStyle(block));
158196

159197
dragger.onDragEnd(dragEvent);
160-
assert.isFalse(this.block.isDeadOrDying());
198+
assert.isFalse(block.isDeadOrDying());
199+
});
200+
201+
suite('Mutator', function () {
202+
/**
203+
* Opens a mutator on a controls_if block and returns the mutator workspace.
204+
*
205+
* @param {!Blockly.WorkspaceSvg} workspace The main workspace.
206+
* @returns {!Promise<!Blockly.WorkspaceSvg>} The mutator workspace.
207+
*/
208+
async function openMutator(workspace) {
209+
const block = Blockly.serialization.blocks.append(
210+
{
211+
'type': 'controls_if',
212+
'extraState': {
213+
'elseIfCount': 0,
214+
},
215+
},
216+
workspace,
217+
);
218+
block.initSvg();
219+
block.render();
220+
const icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE);
221+
await icon.setBubbleVisible(true);
222+
return icon.getWorkspace();
223+
}
224+
225+
test('deletes flyout block when pointer is over flyout delete area at zoomed scale', async function () {
226+
for (let i = 0; i < 3; i++) {
227+
this.workspace.zoomCenter(1);
228+
}
229+
230+
const mutatorWorkspace = await openMutator(this.workspace);
231+
this.clock.runAll();
232+
mutatorWorkspace.recordDragTargets();
233+
234+
const flyout = mutatorWorkspace.getFlyout();
235+
const flyoutRect = flyout.getClientRect();
236+
assert.isNotNull(flyoutRect);
237+
238+
const flyoutBlock = flyout
239+
.getWorkspace()
240+
.getBlocksByType('controls_if_elseif')[0];
241+
flyoutBlock.initSvg();
242+
flyoutBlock.render();
243+
244+
const {dragger, dragEvent, block} = dragBlock(
245+
flyoutBlock,
246+
rectCenterClient(flyoutRect),
247+
);
248+
249+
assert.isTrue(
250+
flyoutRect.contains(dragEvent.clientX, dragEvent.clientY),
251+
'Expected cursor to be inside flyout delete area',
252+
);
253+
assert.isTrue(hasDeleteStyle(block));
254+
255+
dragger.onDragEnd(dragEvent);
256+
assert.isTrue(block.isDeadOrDying());
257+
});
258+
259+
test('does not apply delete styling when only block origin overlaps flyout delete area at zoomed scale', async function () {
260+
for (let i = 0; i < 3; i++) {
261+
this.workspace.zoomCenter(1);
262+
}
263+
264+
const mutatorWorkspace = await openMutator(this.workspace);
265+
this.clock.runAll();
266+
mutatorWorkspace.recordDragTargets();
267+
268+
const flyout = mutatorWorkspace.getFlyout();
269+
const flyoutRect = flyout.getClientRect();
270+
assert.isNotNull(flyoutRect);
271+
272+
const workspaceBlock = mutatorWorkspace.newBlock('controls_if_elseif');
273+
workspaceBlock.initSvg();
274+
workspaceBlock.render();
275+
workspaceBlock.moveBy(200, 50);
276+
277+
const start = blockCenterClient(workspaceBlock);
278+
const originBefore = blockOriginClient(workspaceBlock);
279+
const desiredOrigin = {
280+
x: flyoutRect.right - 5,
281+
y: originBefore.y,
282+
};
283+
const {dragger, dragEvent, block} = dragBlock(workspaceBlock, {
284+
x: start.x + desiredOrigin.x - originBefore.x,
285+
y: start.y + desiredOrigin.y - originBefore.y,
286+
});
287+
288+
const originAfter = blockOriginClient(block);
289+
assert.isTrue(
290+
flyoutRect.contains(originAfter.x, originAfter.y),
291+
'Expected block origin to overlap flyout delete area',
292+
);
293+
assert.isFalse(
294+
flyoutRect.contains(dragEvent.clientX, dragEvent.clientY),
295+
'Expected cursor to be outside flyout delete area',
296+
);
297+
assert.isFalse(hasDeleteStyle(block));
298+
299+
dragger.onDragEnd(dragEvent);
300+
assert.isFalse(block.isDeadOrDying());
301+
});
161302
});
162303
});

0 commit comments

Comments
 (0)