Skip to content

Commit 155df0a

Browse files
committed
Add pan and zoom controls to Crumble (#1040)
1 parent 510f355 commit 155df0a

6 files changed

Lines changed: 115 additions & 40 deletions

File tree

glue/crumble/README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ Crumble is not polished.**
1111

1212
## Index
1313

14+
- [Crumble](#crumble)
15+
- [Index](#index)
1416
- [Accessing Crumble](#accessing-crumble)
1517
- [Using Crumble](#using-crumble)
16-
- [Loading and Saving Circuits](#loading-saving)
17-
- [Keyboard Controls](#keyboard-commands)
18-
- [Mouse Controls](#mouse-commands)
18+
- [Loading and Saving Circuits](#loading-and-saving-circuits)
19+
- [Keyboard Controls](#keyboard-controls)
20+
- [Mouse Controls](#mouse-controls)
1921
- [Building Crumble](#building-crumble)
2022
- [Testing Crumble](#testing-crumble)
2123

@@ -222,6 +224,13 @@ of the **two qubit variant** (c)
222224
of the **square root** (s)
223225
of the **Y gate** (y) (i.e. the gate `SQRT_YY_DAG 1 2`).
224226

227+
**Interaction**
228+
229+
- `Arrow Keys (Up/Down/Left/Right)`: Pan the Planar Layout view.
230+
- `ctrl+0`: Reset View (reset both the planar view and the timeline scroll)
231+
- `ctrl+-`: Zoom out
232+
- `ctrl++` or `ctrl+=`: Zoom in
233+
225234
<a name="mouse-commands"></a>
226235
### Mouse Controls
227236

@@ -245,6 +254,12 @@ box selection action. The specific parity being used depends on context. For
245254
example, when selecting a column of qubits, the row parity is used. When
246255
selecting a 2d region, the subgrid parity is used.
247256

257+
- `shift+MMB` (Middle Mouse Button):
258+
- Drag on the left (planar layout) to pan the planar layout view
259+
- Drag on the right to scroll vertically through the wires
260+
- `ctrl+MMB`:
261+
- Drag vertically on the left (planar layout) to zoom in/out
262+
248263
<a name="building-crumble"></a>
249264
# Building Crumble
250265

glue/crumble/draw/main_draw.js

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ function xyToPos(x, y, scrollX = 0, scrollY = 0, zoomScale = 1.0) {
2020
let roundedY = Math.floor(focusY * 2 + 0.5) / 2;
2121
let centerX = roundedX*pitch;
2222
let centerY = roundedY*pitch;
23-
//TODO: shall we updated rad here?
2423
let scaledRad = rad * zoomScale;
2524
if (Math.abs(centerX - x) <= scaledRad && Math.abs(centerY - y) <= scaledRad && roundedX % 1 === roundedY % 1) {
2625
return [roundedX, roundedY];
@@ -200,6 +199,7 @@ function defensiveDraw(ctx, body) {
200199
*/
201200
function draw(ctx, snap) {
202201
let circuit = snap.circuit;
202+
let zoom = snap.zoomScale || 1.0;
203203

204204
let numPropagatedLayers = 0;
205205
for (let layer of circuit.layers) {
@@ -217,6 +217,16 @@ function draw(ctx, snap) {
217217
let y = circuit.qubitCoordData[2 * q + 1];
218218
return c2dCoordTransform(x, y);
219219
};
220+
let pixelCoords = q => {
221+
let x = circuit.qubitCoordData[2 * q];
222+
let y = circuit.qubitCoordData[2 * q + 1];
223+
return [x * pitch * zoom - OFFSET_X + snap.scrollX, y * pitch * zoom - OFFSET_Y + snap.scrollY];
224+
}
225+
let logicCoords = q => {
226+
let x = circuit.qubitCoordData[2 * q];
227+
let y = circuit.qubitCoordData[2 * q + 1];
228+
return [x * pitch, y * pitch];
229+
}
220230
let propagatedMarkerLayers = /** @type {!Map<!int, !PropagatedPauliFrames>} */ new Map();
221231
for (let mi = 0; mi < numPropagatedLayers; mi++) {
222232
propagatedMarkerLayers.set(mi, PropagatedPauliFrames.fromCircuit(circuit, mi));
@@ -256,13 +266,6 @@ function draw(ctx, snap) {
256266
}
257267

258268
let scaledRad = rad * snap.zoomScale;
259-
//TODO: which font-size shall I use?
260-
//console.log("ctx font", ctx.font);
261-
// if (scaledRad < 2) {
262-
// ctx.font = '10px sans-serif';
263-
// } else {
264-
// ctx.font = `${scaledRad}px sans-serif`;
265-
// }
266269

267270
defensiveDraw(ctx, () => {
268271
ctx.fillStyle = 'white';
@@ -279,13 +282,21 @@ function draw(ctx, snap) {
279282
}
280283
}
281284
}
285+
286+
287+
288+
ctx.save();
289+
ctx.translate(- OFFSET_X + snap.scrollX, - OFFSET_Y + snap.scrollY);
290+
ctx.scale(zoom, zoom);
291+
282292
let polygonMarkers = [...circuit.layers[lastPolygonLayer].markers];
283293
polygonMarkers.sort((a, b) => b.id_targets.length - a.id_targets.length);
284294
for (let op of polygonMarkers) {
285295
if (op.gate.name === 'POLYGON') {
286-
op.id_draw(qubitDrawCoords, ctx);
296+
op.id_draw(logicCoords, ctx);
287297
}
288298
}
299+
ctx.restore();
289300

290301
// Draw the grid of qubits.
291302
defensiveDraw(ctx, () => {
@@ -311,12 +322,8 @@ function draw(ctx, snap) {
311322
}
312323

313324
ctx.strokeStyle = 'black';
314-
for (let qx = 0; qx < 100; qx += qStep) {
315-
let [x, _] = c2dCoordTransform(qx, 0);
316-
let s = `${qx}`;
317-
ctx.fillStyle = 'black';
318-
ctx.fillText(s, x - ctx.measureText(s).width / 2, 15);
319-
for (let qy = qx % 1; qy < 100; qy += qStep * 2) {
325+
for (let qx = 0; qx < 100; qx += 0.5) {
326+
for (let qy = qx % 1; qy < 100; qy += 1) {
320327
let [x, y] = c2dCoordTransform(qx, qy);
321328
ctx.fillStyle = 'white';
322329
let isUnused = !usedQubitCoordSet.has(`${qx},${qy}`);
@@ -338,18 +345,21 @@ function draw(ctx, snap) {
338345
}
339346
}
340347
});
341-
342-
ctx.font = `${Math.max(6, scaledRad)}px sans-serif`;
348+
349+
ctx.save();
350+
ctx.translate(- OFFSET_X + snap.scrollX, - OFFSET_Y + snap.scrollY);
351+
ctx.scale(zoom, zoom);
352+
343353
for (let [mi, p] of propagatedMarkerLayers.entries()) {
344-
drawCrossMarkers(ctx, snap, qubitDrawCoords, p, mi);
354+
drawCrossMarkers(ctx, snap, logicCoords, p, mi);
345355
}
346356

347357
for (let op of circuit.layers[snap.curLayer].iter_gates_and_markers()) {
348358
if (op.gate.name !== 'POLYGON') {
349-
op.id_draw(qubitDrawCoords, ctx);
359+
op.id_draw(logicCoords, ctx);
350360
}
351361
}
352-
362+
353363
defensiveDraw(ctx, () => {
354364
ctx.globalAlpha *= 0.25
355365
for (let [qx, qy] of snap.timelineSet.values()) {
@@ -359,6 +369,10 @@ function draw(ctx, snap) {
359369
}
360370
});
361371

372+
drawMarkers(ctx, snap, logicCoords, propagatedMarkerLayers);
373+
374+
ctx.restore();
375+
362376
defensiveDraw(ctx, () => {
363377
ctx.globalAlpha *= 0.5
364378
for (let [qx, qy] of snap.focusedSet.values()) {
@@ -368,8 +382,6 @@ function draw(ctx, snap) {
368382
}
369383
});
370384

371-
drawMarkers(ctx, snap, qubitDrawCoords, propagatedMarkerLayers);
372-
373385
if (focusX !== undefined) {
374386
ctx.save();
375387
ctx.globalAlpha *= 0.5;
@@ -402,7 +414,7 @@ function draw(ctx, snap) {
402414
ctx.fillRect(x - scaledRad, y - scaledRad, 2 * scaledRad, 2 * scaledRad);
403415
}
404416
});
405-
417+
406418
ctx.font = `10px sans-serif`;
407419
});
408420

glue/crumble/draw/state_snapshot.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class StateSnapshot {
1818
* @param {!number} mouseDownY
1919
* @param {!Array<![!number, !number]>} boxHighlightPreview
2020
*/
21-
constructor(circuit, curLayer, focusedSet, timelineSet, curMouseX, curMouseY, mouseDownX, mouseDownY, scrollX, scrollY, zoomScale, boxHighlightPreview) {
21+
constructor(circuit, curLayer, focusedSet, timelineSet, curMouseX, curMouseY, mouseDownX, mouseDownY, scrollX, scrollY, timelineScrollY, zoomScale, boxHighlightPreview) {
2222
this.circuit = circuit.copy();
2323
this.curLayer = curLayer;
2424
this.focusedSet = new Map(focusedSet.entries());
@@ -29,6 +29,7 @@ class StateSnapshot {
2929
this.mouseDownY = mouseDownY;
3030
this.scrollX = scrollX || 0;
3131
this.scrollY = scrollY || 0;
32+
this.timelineScrollY = timelineScrollY || 0;
3233
this.zoomScale = zoomScale || 1.0;
3334
this.boxHighlightPreview = [...boxHighlightPreview];
3435

glue/crumble/draw/timeline_viewer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFun
129129
cur_x += rad * 0.25;
130130
cur_run++;
131131
}
132-
base_y2xy.set(`${x},${y}`, [Math.round(cur_x) + 0.5, Math.round(cur_y) + 0.5]);
132+
base_y2xy.set(`${x},${y}`, [Math.round(cur_x) + 0.5, Math.round(cur_y + snap.timelineScrollY) + 0.5]);
133133
}
134134

135135
let x_pitch = TIMELINE_PITCH + Math.ceil(rad*max_run*0.25);

glue/crumble/editor/editor_state.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class EditorState {
4848
this.mouseDownY = /** @type {undefined|!number} */ undefined;
4949
this.scrollX = 0;
5050
this.scrollY = 0;
51+
this.timelineScrollY = 0;
5152
this.zoomScale = 1.0;
5253
this.obs_val_draw_state = /** @type {!ObservableValue<StateSnapshot>} */ new ObservableValue(this.toSnapshot(undefined));
5354
}
@@ -215,6 +216,7 @@ class EditorState {
215216
this.mouseDownY,
216217
this.scrollX,
217218
this.scrollY,
219+
this.timelineScrollY,
218220
this.zoomScale,
219221
this.currentPositionsBoxesByMouseDrag(this.chorder.curModifiers.has("alt")),
220222
);
@@ -248,6 +250,7 @@ class EditorState {
248250
let mouseDownY = this.mouseDownY;
249251
let scrollX = this.scrollX || 0;
250252
let scrollY = this.scrollY || 0;
253+
let timelineScrollY = this.timelineScrollY || 0;
251254
let zoomScale = this.zoomScale || 1.0;
252255
let result = [];
253256
if (curMouseX !== undefined && mouseDownX !== undefined) {
@@ -273,7 +276,9 @@ class EditorState {
273276
b = 2;
274277
}
275278
for (let x = x1; x <= x2; x += 0.5) {
279+
if (x < 0) continue;
276280
for (let y = y1; y <= y2; y += 0.5) {
281+
if (y < 0) continue;
277282
if (x % 1 === y % 1) {
278283
if (!parityLock || (sx % b === x % b && sy % b === y % b)) {
279284
result.push([x, y]);
@@ -329,26 +334,36 @@ class EditorState {
329334
this.scrollX += dx;
330335
this.scrollY += dy;
331336
this.force_redraw();
332-
console.log("pan", dx, dy, this.scrollX, this.scrollY);
333337
}
334-
338+
/**
339+
* @param {!number} dy
340+
*/
341+
timelinePan(dy) {
342+
if( this.timelineScrollY + dy > 0) {
343+
return;
344+
}
345+
this.timelineScrollY += dy;
346+
this.force_redraw();
347+
}
348+
/**
349+
* @param {!number} factor
350+
* @returns
351+
*/
335352
zoom(factor) {
336353
if( this.zoomScale * factor < 0.1 || this.zoomScale * factor > 10) {
337354
return;
338355
}
339356
this.zoomScale *= factor;
340357
let mouseX = this.curMouseX || 0;
341358
let mouseY = this.curMouseY || 0;
342-
this.scrollX = mouseX - (mouseX - this.scrollX) * factor;// + OFFSET_X;
343-
this.scrollY = mouseY - (mouseY - this.scrollY) * factor;// + OFFSET_Y;
344-
console.log("zoom", this.zoomScale, this.scrollX, this.scrollY);
359+
this.scrollX = mouseX - (mouseX - this.scrollX) * factor;
360+
this.scrollY = mouseY - (mouseY - this.scrollY) * factor;
345361
this.force_redraw();
346-
//TODO
347-
348362
}
349363
resetView() {
350364
this.scrollX = 0;
351365
this.scrollY = 0;
366+
this.timelineScrollY = 0;
352367
this.zoomScale = 1.0;
353368
this.force_redraw();
354369
}

glue/crumble/main.js

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,19 @@ editorState.canvas.addEventListener('mousemove', ev => {
157157
lastPanY = ev.clientY;
158158
return;
159159
}
160+
if (isTimelinePanning) {
161+
const dy = ev.clientY - lastPanY;
162+
editorState.timelinePan(dy);
163+
lastPanY = ev.clientY;
164+
editorState.force_redraw();
165+
return;
166+
}
167+
if (isZooming) {
168+
const dy = ev.clientY - lastZoomY;
169+
editorState.zoom(1.0 + dy * 0.01);
170+
lastZoomY = ev.clientY;
171+
return;
172+
}
160173

161174
editorState.curMouseX = ev.offsetX + OFFSET_X;
162175
editorState.curMouseY = ev.offsetY + OFFSET_Y;
@@ -172,9 +185,12 @@ editorState.canvas.addEventListener('mousemove', ev => {
172185
});
173186

174187
let isInScrubber = false;
175-
let isPanning = false; // add scrolling by dragging with MMB, no touchpad support yet
188+
let isPanning = false; // Add scrolling by dragging with shift+MMB, no touchpad support yet
189+
let isTimelinePanning = false; // Add vertical scrolling of the timeline by dragging with shift+MMB in the timeline area, no touchpad support yet
176190
let lastPanX = 0;
177191
let lastPanY = 0;
192+
let isZooming = false; // Add zooming by dragging with ctrl+MMB, no touchpad support yet
193+
let lastZoomY = 0;
178194
editorState.canvas.addEventListener('mousedown', ev => {
179195
editorState.curMouseX = ev.offsetX + OFFSET_X;
180196
editorState.curMouseY = ev.offsetY + OFFSET_Y;
@@ -191,25 +207,41 @@ editorState.canvas.addEventListener('mousedown', ev => {
191207

192208

193209
if (ev.button === 1 && ev.shiftKey) {
194-
if(ev.offsetX <= w){
210+
if(ev.offsetX <= w){ // Panning for the planar view
195211
isPanning = true;
196212
lastPanX = ev.clientX;
197213
lastPanY = ev.clientY;
198214
ev.preventDefault();
199215
return;
200216
}
201-
else{
202-
//TODO
217+
else{ // Panning for the timeline view
218+
isTimelinePanning = true;
219+
lastPanY = ev.clientY;
220+
ev.preventDefault();
221+
return;
203222
}
204223
}
205224

225+
if (ev.button === 1 && ev.ctrlKey && ev.offsetX <= w) {
226+
isZooming = true;
227+
lastZoomY = ev.clientY;
228+
ev.preventDefault();
229+
return;
230+
}
231+
232+
206233

207234
editorState.force_redraw();
208235
});
209236

210237
editorState.canvas.addEventListener('mouseup', ev => {
211-
if (isPanning && ev.button === 1) {
238+
if ((isPanning || isTimelinePanning || isZooming) && ev.button === 1) {
212239
isPanning = false;
240+
isTimelinePanning = false;
241+
isZooming = false;
242+
243+
editorState.mouseDownX = undefined;
244+
editorState.mouseDownY = undefined;
213245
return;
214246
}
215247
let highlightedArea = editorState.currentPositionsBoxesByMouseDrag(ev.altKey);

0 commit comments

Comments
 (0)