From a6577856845c8eb90200fa241fda5dd374efb744 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 7 Jun 2022 13:58:03 -0400 Subject: [PATCH 001/108] Allow gates draggable --- example/script.js | 2 +- src/circuit.ts | 8 +- src/editable.ts | 302 ++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 4 +- src/sqore.ts | 23 ++-- 5 files changed, 323 insertions(+), 16 deletions(-) create mode 100644 src/editable.ts diff --git a/example/script.js b/example/script.js index f6d62df9..ae8c42c3 100644 --- a/example/script.js +++ b/example/script.js @@ -12,7 +12,7 @@ if (typeof qviz != 'undefined') { const sampleDiv = document.getElementById('sample'); if (sampleDiv != null) { - qviz.draw(sample, sampleDiv, qviz.STYLES['Default']); + qviz.draw(sample, sampleDiv, qviz.STYLES['Default'], 0, true); } const teleportDiv = document.getElementById('teleport'); diff --git a/src/circuit.ts b/src/circuit.ts index 685215e3..2163c8e5 100644 --- a/src/circuit.ts +++ b/src/circuit.ts @@ -54,13 +54,13 @@ export interface Operation { /** Nested operations within this operation. */ children?: Operation[]; /** Whether gate is a measurement operation. */ - isMeasurement: boolean; + isMeasurement?: boolean; /** Whether gate is a conditional operation. */ - isConditional: boolean; + isConditional?: boolean; /** Whether gate is a controlled operation. */ - isControlled: boolean; + isControlled?: boolean; /** Whether gate is an adjoint operation. */ - isAdjoint: boolean; + isAdjoint?: boolean; /** Control registers the gate acts on. */ controls?: Register[]; /** Target registers the gate acts on. */ diff --git a/src/editable.ts b/src/editable.ts new file mode 100644 index 00000000..5f56f04d --- /dev/null +++ b/src/editable.ts @@ -0,0 +1,302 @@ +import { Operation } from './circuit'; +import { leftPadding } from './constants'; +import { box } from './formatters/formatUtils'; +import { Sqore } from './sqore'; + +interface Context { + container: HTMLElement; + operations: Operation[]; + wires: Wires; + renderFn: () => void; +} + +interface Wires { + [y: string]: string; +} + +let _sourceTarget: SVGElement | null; + +const addEditable = (container: HTMLElement, sqore: Sqore): void => { + const context: Context = { + container: container, + operations: sqore.circuit.operations, + wires: getWireElemsY(container), + renderFn: () => sqore.draw(container, 0, true), + }; + addCustomStyles(container); + addDropzones(container); + addDocumentEvents(container); + addDropzoneEvents(context); + addMouseEvents(context); +}; + +// Commands + +const addCustomStyles = (container: HTMLElement) => { + const style = container.querySelector('style'); + if (style) { + style.innerHTML += ` + .dropzone { + fill: transparent; + stroke: transparent; + } + .dropzone:hover{ + fill: red; + opacity: 25%; + } + text { + user-select: none; + } + .copying { + cursor: copy; + } + .moving { + cursor: move; + } + .detail-panel { + display: flex; + align-content: center; + gap: 12px; + } + `; + } +}; + +const addDropzones = (container: HTMLElement) => { + const gateElems = getGateElems(container); + gateElems.forEach((gateElem) => { + const { x, y, width, height } = gateElem.getBBox({ stroke: true }); + const dataId = getDataId(gateElem); + gateElem.append(createLeftDropzone(x, y, height, dataId)); + gateElem.append(createRightDropzone(x, y, width, height, dataId)); + }); +}; + +const addDocumentEvents = (container: HTMLElement) => { + container.addEventListener('click', (ev: MouseEvent) => { + _sourceTarget = null; + if (ev.ctrlKey) return; + }); + container.addEventListener('contextmenu', (ev: MouseEvent) => { + ev.preventDefault(); + }); + container.addEventListener('mouseup', () => { + cursorCopy(container, false); + cursorMove(container, false); + }); +}; + +const addDropzoneEvents = (context: Context) => { + const { container } = context; + const dropzoneElems = container.querySelectorAll('.dropzone'); + dropzoneElems.forEach((dropzoneElem) => { + dropzoneElem.addEventListener('mouseup', (ev: MouseEvent) => handleDropzoneMouseUp(ev, context)); + }); +}; + +const addMouseEvents = (context: Context) => { + const { container } = context; + const gateElems = getGateElems(container); + gateElems.forEach((gateElem) => { + gateElem.addEventListener('mousedown', (ev: MouseEvent) => handleGateMouseDown(ev, container)); + }); +}; + +// Event handlers +const handleGateMouseDown = (ev: MouseEvent, container: HTMLElement) => { + ev.stopPropagation(); + _sourceTarget = ev.currentTarget as SVGGElement; + + // Ctrl + Mousedown to copy. Mousedown only to move. + ev.ctrlKey ? cursorCopy(container, true) : cursorMove(container, true); +}; + +const handleDropzoneMouseUp = (ev: MouseEvent, context: Context) => { + ev.stopPropagation(); + + const { container, operations, wires, renderFn } = context; + + const currentTarget = ev.currentTarget as SVGGElement; + + if (!currentTarget) return false; + + const dataId = getDataId(currentTarget); + const parent = getParent(dataId, operations); + const index = splitDataId(dataId).pop(); + const position = getDropzonePosition(currentTarget); + + if (_sourceTarget == null) return false; + + const sourceDataId = getDataId(_sourceTarget); + const sourceParent = getParent(sourceDataId, operations); + const sourceIndex = splitDataId(sourceDataId).pop(); + + if (index == null || sourceIndex == null) return false; + + const newGate = getGate(sourceDataId, operations); + const wireY = getClosestWireY(ev.offsetY, wires); + + // Not allow Measure gate to move vertically + if (wireY != null && newGate.gate !== 'measure') { + console.log(wires[wireY]); + // wires[wireY] returns qubit name (i.e: 'q0') + // this remove 'q' and assign an index (i.e: 0) + const index = Number(wires[wireY].slice(1)); + const [firstTarget, ...targetsExceptFirst] = newGate.targets; + // Reserve all other properties, only change qId + Object.assign(firstTarget, { ...firstTarget, qId: index }); + // Reserve all other targets, only change first target + Object.assign(newGate, { ...newGate, targets: [firstTarget, ...targetsExceptFirst] }); + } + + // Remove source element if moving using Ctrl + Mousedown + if (!ev.ctrlKey) { + deleteAt(sourceParent, sourceIndex); + } + + // If dropzone is left of gate, insert before gate. + // Otherwise, insert after. + if (position === 'left') { + insertBefore(parent, index, newGate); + } else { + insertAfter(parent, index, newGate); + } + + // Remove cursor styles + cursorCopy(container, false); + cursorMove(container, false); + + // Redraw the circuit + renderFn(); +}; + +// Element getters + +const getGateElems = (container: HTMLElement): SVGGElement[] => { + return Array.from(container.querySelectorAll('g.gate')); +}; + +const getWireElems = (container: HTMLElement) => { + // elems include qubit wires and lines of measure gates + const elems = container.querySelectorAll('svg > g:nth-child(3) > g'); + // filter out elements having more than 2 elements because + // qubit wires contain only 2 elements: and + // lines of measure gates contain 4 elements + return Array.from(elems).filter((elem) => elem.childElementCount < 3); +}; + +// Element creators + +const createDropzone = ( + x: number, + y: number, + width: number, + height: number, + dataId: string, + position: 'left' | 'right', +): SVGElement => { + const dropzone = box(x, y, width, height, `dropzone`); + dropzone.setAttribute('data-id', dataId); + dropzone.setAttribute('data-dropzone-position', position); + return dropzone; +}; + +const createLeftDropzone = (x: number, y: number, height: number, dataId: string): SVGElement => { + return createDropzone(x - leftPadding / 2, y, leftPadding / 2, height, dataId, 'left'); +}; +const createRightDropzone = (x: number, y: number, width: number, height: number, dataId: string): SVGElement => { + return createDropzone(x + width, y, leftPadding / 2, height, dataId, 'right'); +}; + +// Operation getters + +const getParent = (dataId: string, operations: Operation[]): Operation[] => { + const segments = splitDataId(dataId); + // Remove last segment to navigate to parent instead of child + segments.pop(); + + let parent = operations; + for (const segment of segments) { + parent = parent[segment].children || parent; + } + return parent; +}; + +const getGate = (dataId: string, operations: Operation[]): Operation => { + const parent = getParent(dataId, operations); + const index = splitDataId(dataId).pop(); + + if (index == null) { + throw new Error('Gate not found'); + } + + return parent[index]; +}; + +// Utilities +const getDataId = (element: Element): string => { + return element.getAttribute('data-id') || ''; +}; + +const splitDataId = (dataId: string): number[] => { + return dataId.split('-').map(Number); +}; + +const getWireElemsY = (container: HTMLElement): Wires => { + const wireElems = getWireElems(container); + return wireElems.reduce((previous, current) => { + const y = getWireElemY(current); + const text = getWireElemText(current); + return { ...previous, [`${y}`]: text }; + }, {}); +}; + +const getWireElemY = (wireElem: SVGGElement): number => { + const lineElem = wireElem.querySelector('line'); + if (lineElem == null || lineElem.y1.baseVal.value == null) throw Error('y not found'); + return lineElem.y1.baseVal.value; +}; +const getWireElemText = (wireElem: SVGGElement): string => { + const textElem = wireElem.querySelector('text'); + if (textElem == null || textElem.textContent == null) throw new Error('Text not found'); + return textElem.textContent; +}; + +const getClosestWireY = (offsetY: number, wires: { [y: number]: string }) => { + let wireY; + Object.entries(wires).forEach((wire) => { + const y = wire[0]; + const distance = Math.abs(offsetY - Number(y)); + // 15 is a magic number + if (distance < 15) { + wireY = y; + } + }); + return wireY || null; +}; + +const getDropzonePosition = (element: SVGElement) => { + return element.getAttribute('data-dropzone-position'); +}; + +const insertBefore = (parent: Operation[], index: number, newGate: Operation): void => { + parent.splice(index, 0, newGate); +}; + +const insertAfter = (parent: Operation[], index: number, newGate: Operation): void => { + parent.splice(index + 1, 0, newGate); +}; + +const deleteAt = (parent: Operation[], index: number): void => { + parent.splice(index, 1); +}; + +const cursorMove = (container: HTMLElement, value: boolean) => { + value ? container.classList.add('moving') : container.classList.remove('moving'); +}; + +const cursorCopy = (container: HTMLElement, value: boolean) => { + value ? container.classList.add('copying') : container.classList.remove('copying'); +}; + +export { addEditable }; diff --git a/src/index.ts b/src/index.ts index 0a040268..8a9093bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,9 +18,11 @@ export const draw = ( container: HTMLElement, style: StyleConfig | string = {}, renderDepth = 0, + editable = false, ): void => { const sqore = new Sqore(circuit, style); - sqore.draw(container, renderDepth); + + sqore.draw(container, renderDepth, editable); }; export { STYLES } from './styles'; diff --git a/src/sqore.ts b/src/sqore.ts index c20ba65c..e0d85dfc 100644 --- a/src/sqore.ts +++ b/src/sqore.ts @@ -10,6 +10,7 @@ import { Metadata, GateType } from './metadata'; import { StyleConfig, style, STYLES } from './styles'; import { createUUID } from './utils'; import { svgNS } from './constants'; +import { addEditable } from './editable'; /** * Contains metadata for visualization. @@ -56,7 +57,7 @@ export class Sqore { * @param container HTML element for rendering visualization into. * @param renderDepth Initial layer depth at which to render gates. */ - draw(container: HTMLElement, renderDepth = 0): void { + draw(container: HTMLElement, renderDepth = 0, editable = false): void { // Inject into container if (container == null) throw new Error(`Container not provided.`); @@ -78,8 +79,7 @@ export class Sqore { const id: string = circuit.operations[0].dataAttributes['id']; this.expandOperation(circuit.operations, id); } - - this.renderCircuit(container, circuit); + this.renderCircuit(container, circuit, editable); } /** @@ -107,13 +107,17 @@ export class Sqore { * @param container HTML element for rendering visualization into. * @param circuit Circuit object to be rendered. */ - private renderCircuit(container: HTMLElement, circuit: Circuit): void { + public renderCircuit(container: HTMLElement, circuit: Circuit, editable: boolean): void { // Create visualization components const composedSqore: ComposedSqore = this.compose(circuit); const svg: SVGElement = this.generateSvg(composedSqore); container.innerHTML = ''; container.appendChild(svg); - this.addGateClickHandlers(container, circuit); + this.addGateClickHandlers(container, circuit, editable); + + if (editable) { + addEditable(container, this); + } } /** @@ -231,9 +235,9 @@ export class Sqore { * @param circuit Circuit to be visualized. * */ - private addGateClickHandlers(container: HTMLElement, circuit: Circuit): void { + private addGateClickHandlers(container: HTMLElement, circuit: Circuit, editable: boolean): void { this.addClassicalControlHandlers(container); - this.addZoomHandlers(container, circuit); + this.addZoomHandlers(container, circuit, editable); } /** @@ -291,7 +295,7 @@ export class Sqore { * @param circuit Circuit to be visualized. * */ - private addZoomHandlers(container: HTMLElement, circuit: Circuit): void { + private addZoomHandlers(container: HTMLElement, circuit: Circuit, editable: boolean): void { container.querySelectorAll('.gate .gate-control').forEach((ctrl) => { // Zoom in on clicked gate ctrl.addEventListener('click', (ev: Event) => { @@ -302,8 +306,7 @@ export class Sqore { } else if (ctrl.classList.contains('gate-expand')) { this.expandOperation(circuit.operations, gateId); } - this.renderCircuit(container, circuit); - + this.renderCircuit(container, circuit, editable); ev.stopPropagation(); } }); From 9a66bc84a6e4b5854bc0a3da9ef77330b8285edb Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 7 Jun 2022 14:08:48 -0400 Subject: [PATCH 002/108] Add description to addEditable function --- src/editable.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/editable.ts b/src/editable.ts index 5f56f04d..68c0b340 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -16,6 +16,14 @@ interface Wires { let _sourceTarget: SVGElement | null; +/** + * Add editable elements and events. + * + * @param container HTML element for rendering visualization into. + * @param sqore Sqore object + * + */ + const addEditable = (container: HTMLElement, sqore: Sqore): void => { const context: Context = { container: container, From d831c48717d4cb063e53a595b83acd01e9f6d1d5 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 7 Jun 2022 14:09:07 -0400 Subject: [PATCH 003/108] Add return types --- src/editable.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/editable.ts b/src/editable.ts index 68c0b340..9e900205 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -40,7 +40,7 @@ const addEditable = (container: HTMLElement, sqore: Sqore): void => { // Commands -const addCustomStyles = (container: HTMLElement) => { +const addCustomStyles = (container: HTMLElement): void => { const style = container.querySelector('style'); if (style) { style.innerHTML += ` @@ -70,7 +70,7 @@ const addCustomStyles = (container: HTMLElement) => { } }; -const addDropzones = (container: HTMLElement) => { +const addDropzones = (container: HTMLElement): void => { const gateElems = getGateElems(container); gateElems.forEach((gateElem) => { const { x, y, width, height } = gateElem.getBBox({ stroke: true }); @@ -80,7 +80,7 @@ const addDropzones = (container: HTMLElement) => { }); }; -const addDocumentEvents = (container: HTMLElement) => { +const addDocumentEvents = (container: HTMLElement): void => { container.addEventListener('click', (ev: MouseEvent) => { _sourceTarget = null; if (ev.ctrlKey) return; @@ -94,7 +94,7 @@ const addDocumentEvents = (container: HTMLElement) => { }); }; -const addDropzoneEvents = (context: Context) => { +const addDropzoneEvents = (context: Context): void => { const { container } = context; const dropzoneElems = container.querySelectorAll('.dropzone'); dropzoneElems.forEach((dropzoneElem) => { @@ -102,7 +102,7 @@ const addDropzoneEvents = (context: Context) => { }); }; -const addMouseEvents = (context: Context) => { +const addMouseEvents = (context: Context): void => { const { container } = context; const gateElems = getGateElems(container); gateElems.forEach((gateElem) => { @@ -111,7 +111,7 @@ const addMouseEvents = (context: Context) => { }; // Event handlers -const handleGateMouseDown = (ev: MouseEvent, container: HTMLElement) => { +const handleGateMouseDown = (ev: MouseEvent, container: HTMLElement): void => { ev.stopPropagation(); _sourceTarget = ev.currentTarget as SVGGElement; @@ -119,7 +119,7 @@ const handleGateMouseDown = (ev: MouseEvent, container: HTMLElement) => { ev.ctrlKey ? cursorCopy(container, true) : cursorMove(container, true); }; -const handleDropzoneMouseUp = (ev: MouseEvent, context: Context) => { +const handleDropzoneMouseUp = (ev: MouseEvent, context: Context): void | false => { ev.stopPropagation(); const { container, operations, wires, renderFn } = context; @@ -184,7 +184,7 @@ const getGateElems = (container: HTMLElement): SVGGElement[] => { return Array.from(container.querySelectorAll('g.gate')); }; -const getWireElems = (container: HTMLElement) => { +const getWireElems = (container: HTMLElement): SVGGElement[] => { // elems include qubit wires and lines of measure gates const elems = container.querySelectorAll('svg > g:nth-child(3) > g'); // filter out elements having more than 2 elements because @@ -270,7 +270,7 @@ const getWireElemText = (wireElem: SVGGElement): string => { return textElem.textContent; }; -const getClosestWireY = (offsetY: number, wires: { [y: number]: string }) => { +const getClosestWireY = (offsetY: number, wires: { [y: number]: string }): number | null => { let wireY; Object.entries(wires).forEach((wire) => { const y = wire[0]; @@ -283,8 +283,10 @@ const getClosestWireY = (offsetY: number, wires: { [y: number]: string }) => { return wireY || null; }; -const getDropzonePosition = (element: SVGElement) => { - return element.getAttribute('data-dropzone-position'); +const getDropzonePosition = (element: SVGElement): string => { + const position = element.getAttribute('data-dropzone-position'); + if (position == null) throw new Error('Position not found'); + return position; }; const insertBefore = (parent: Operation[], index: number, newGate: Operation): void => { @@ -299,11 +301,11 @@ const deleteAt = (parent: Operation[], index: number): void => { parent.splice(index, 1); }; -const cursorMove = (container: HTMLElement, value: boolean) => { +const cursorMove = (container: HTMLElement, value: boolean): void => { value ? container.classList.add('moving') : container.classList.remove('moving'); }; -const cursorCopy = (container: HTMLElement, value: boolean) => { +const cursorCopy = (container: HTMLElement, value: boolean): void => { value ? container.classList.add('copying') : container.classList.remove('copying'); }; From 13108145ce9b22e6b5b87b976077e075a6c0d214 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 7 Jun 2022 20:30:26 -0400 Subject: [PATCH 004/108] Add tests --- __tests__/__snapshots__/editable.test.ts.snap | 65 ++++ __tests__/editable.test.ts | 361 ++++++++++++++++++ src/editable.ts | 19 +- 3 files changed, 442 insertions(+), 3 deletions(-) create mode 100644 __tests__/__snapshots__/editable.test.ts.snap create mode 100644 __tests__/editable.test.ts diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap new file mode 100644 index 00000000..637f61b6 --- /dev/null +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing cursorCopy turn off copy cursor 1`] = ` +
+`; + +exports[`Testing cursorCopy turn off copy cursor 2`] = ` +
+`; + +exports[`Testing cursorCopy turn on and off copy cursor 1`] = ` +
+`; + +exports[`Testing cursorCopy turn on and off copy cursor 2`] = ` +
+`; + +exports[`Testing cursorCopy turn on copy cursor 1`] = `
`; + +exports[`Testing cursorCopy turn on copy cursor 2`] = ` +
+`; + +exports[`Testing cursorMove turn off move cursor 1`] = ` +
+`; + +exports[`Testing cursorMove turn off move cursor 2`] = ` +
+`; + +exports[`Testing cursorMove turn on and off move cursor 1`] = ` +
+`; + +exports[`Testing cursorMove turn on and off move cursor 2`] = ` +
+`; + +exports[`Testing cursorMove turn on move cursor 1`] = `
`; + +exports[`Testing cursorMove turn on move cursor 2`] = ` +
+`; diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts new file mode 100644 index 00000000..34dd2be2 --- /dev/null +++ b/__tests__/editable.test.ts @@ -0,0 +1,361 @@ +import { Operation } from '../src/circuit'; +import { exportedForTesting } from '../src/editable'; +import { RegisterType } from '../src/register'; + +const { + getDataId, + splitDataId, + cursorMove, + cursorCopy, + deleteAt, + insertBefore, + insertAfter, + getDropzonePosition, + getWireElemText, +} = exportedForTesting; + +// Utlities +describe('Testing getDataId', () => { + const elem = document.createElement('div'); + test('with with no data-id', () => { + expect(getDataId(elem)).toBe(''); + }); + test('with level 0 data-id', () => { + elem.setAttribute('data-id', '0'); + expect(getDataId(elem)).toBe('0'); + }); + + test('with level 1 data-id', () => { + elem.setAttribute('data-id', '0-1'); + expect(getDataId(elem)).toBe('0-1'); + }); +}); + +describe('Testing splitDataId', () => { + test('with empty dataId', () => { + expect(splitDataId('')).toStrictEqual([]); + }); + test('with level 0 data-id', () => { + expect(splitDataId('1')).toStrictEqual([1]); + }); + + test('with level 1 data-id', () => { + expect(splitDataId('1-2')).toStrictEqual([1, 2]); + }); +}); + +describe('Testing cursorMove', () => { + const container = document.createElement('div'); + test('turn on move cursor', () => { + expect(container).toMatchSnapshot(); + cursorMove(container, true); + expect(container).toMatchSnapshot(); + }); + test('turn off move cursor', () => { + expect(container).toMatchSnapshot(); + cursorMove(container, false); + expect(container).toMatchSnapshot(); + }); + test('turn on and off move cursor', () => { + expect(container).toMatchSnapshot(); + cursorMove(container, true); + cursorMove(container, false); + expect(container).toMatchSnapshot(); + }); +}); + +describe('Testing cursorCopy', () => { + const container = document.createElement('div'); + test('turn on copy cursor', () => { + expect(container).toMatchSnapshot(); + cursorCopy(container, true); + expect(container).toMatchSnapshot(); + }); + test('turn off copy cursor', () => { + expect(container).toMatchSnapshot(); + cursorCopy(container, false); + expect(container).toMatchSnapshot(); + }); + test('turn on and off copy cursor', () => { + expect(container).toMatchSnapshot(); + cursorCopy(container, true); + cursorCopy(container, false); + expect(container).toMatchSnapshot(); + }); +}); + +describe('Testing deleteAt', () => { + const operations: Operation[] = []; + beforeEach(() => { + Object.assign(operations, [ + { + gate: 'X', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + { + gate: 'Y', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + { + gate: 'Z', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + ]); + }); + test('delete X at index 0', () => { + deleteAt(operations, 0); + expect(operations).toStrictEqual([ + { + gate: 'Y', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + { + gate: 'Z', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + ]); + }); + test('delete Y at index 1', () => { + deleteAt(operations, 1); + expect(operations).toStrictEqual([ + { + gate: 'X', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + { + gate: 'Z', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + ]); + }); + test('delete Z and X at index 2, 0', () => { + deleteAt(operations, 2); + deleteAt(operations, 0); + expect(operations).toStrictEqual([ + { + gate: 'Y', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + ]); + }); +}); + +describe('Testing insertBefore', () => { + test('insert before X', () => { + const operations = [ + { + gate: 'X', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + { + gate: 'Z', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + ]; + const newGate = { + gate: 'Y', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }; + insertBefore(operations, 0, newGate); + expect(operations).toStrictEqual([ + { + gate: 'Y', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + { + gate: 'X', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + { + gate: 'Z', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + ]); + }); +}); + +describe('Testing insertAfter', () => { + test('insert after X', () => { + const operations = [ + { + gate: 'X', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + { + gate: 'Z', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + ]; + const newGate = { + gate: 'Y', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }; + insertAfter(operations, 0, newGate); + expect(operations).toStrictEqual([ + { + gate: 'X', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + { + gate: 'Y', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + { + gate: 'Z', + isMeasurement: false, + isConditional: false, + isControlled: false, + isAdjoint: false, + controls: [], + targets: [{ type: RegisterType.Qubit, qId: 0 }], + }, + ]); + }); +}); + +describe('Testing getDropzonePosition', () => { + let svgElem: SVGElement; + let dropzoneElem: SVGElement; + beforeEach(() => { + svgElem = document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGElement; + dropzoneElem = document.createElementNS('http://www.w3.org/2000/svg', 'rect') as SVGElement; + svgElem.append(dropzoneElem); + }); + test('get position of non-dropzone', () => { + expect(() => getDropzonePosition(dropzoneElem)).toThrowError('Position not found'); + }); + test('get position of dropzone on the left', () => { + dropzoneElem.setAttribute('data-dropzone-position', 'left'); + expect(getDropzonePosition(dropzoneElem)).toBe('left'); + }); + test('get position of dropzone on the right', () => { + dropzoneElem.setAttribute('data-dropzone-position', 'right'); + expect(getDropzonePosition(dropzoneElem)).toBe('right'); + }); +}); + +describe('Testing getWireElementText', () => { + let svgElem: SVGElement; + let groupElem: SVGGElement; + let textElem: SVGGElement; + beforeEach(() => { + svgElem = document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGElement; + groupElem = document.createElementNS('http://www.w3.org/2000/svg', 'g') as SVGGElement; + textElem = document.createElementNS('http://www.w3.org/2000/svg', 'text') as SVGTextElement; + groupElem.append(textElem); + svgElem.append(groupElem); + }); + test('text element not exists', () => { + textElem.remove(); + expect(() => getWireElemText(groupElem)).toThrowError('Text not found'); + }); + test('get text element without textContent', () => { + expect(() => getWireElemText(groupElem)).toThrowError('Text not found'); + }); + test('get text element empty textContent', () => { + expect(() => getWireElemText(groupElem)).toThrowError('Text not found'); + }); + test('should return q0', () => { + textElem.textContent = 'q0'; + expect(getWireElemText(groupElem)).toEqual('q0'); + }); + test('should return q1', () => { + textElem.textContent = 'q1'; + expect(getWireElemText(groupElem)).toEqual('q1'); + }); +}); diff --git a/src/editable.ts b/src/editable.ts index 9e900205..0cfa2adf 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -247,7 +247,7 @@ const getDataId = (element: Element): string => { }; const splitDataId = (dataId: string): number[] => { - return dataId.split('-').map(Number); + return dataId === '' ? [] : dataId.split('-').map(Number); }; const getWireElemsY = (container: HTMLElement): Wires => { @@ -266,7 +266,8 @@ const getWireElemY = (wireElem: SVGGElement): number => { }; const getWireElemText = (wireElem: SVGGElement): string => { const textElem = wireElem.querySelector('text'); - if (textElem == null || textElem.textContent == null) throw new Error('Text not found'); + if (textElem == null || textElem.textContent == null || textElem.textContent === '') + throw new Error('Text not found'); return textElem.textContent; }; @@ -309,4 +310,16 @@ const cursorCopy = (container: HTMLElement, value: boolean): void => { value ? container.classList.add('copying') : container.classList.remove('copying'); }; -export { addEditable }; +const exportedForTesting = { + getDataId, + splitDataId, + cursorMove, + cursorCopy, + deleteAt, + insertBefore, + insertAfter, + getDropzonePosition, + getWireElemText, +}; + +export { addEditable, exportedForTesting }; From 7c3d7329d92f4e39d4440cd8048cade877a40fbf Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Wed, 8 Jun 2022 12:57:51 -0400 Subject: [PATCH 005/108] Revert renderCircuit to private --- src/sqore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sqore.ts b/src/sqore.ts index e0d85dfc..bfb63e98 100644 --- a/src/sqore.ts +++ b/src/sqore.ts @@ -107,7 +107,7 @@ export class Sqore { * @param container HTML element for rendering visualization into. * @param circuit Circuit object to be rendered. */ - public renderCircuit(container: HTMLElement, circuit: Circuit, editable: boolean): void { + private renderCircuit(container: HTMLElement, circuit: Circuit, editable: boolean): void { // Create visualization components const composedSqore: ComposedSqore = this.compose(circuit); const svg: SVGElement = this.generateSvg(composedSqore); From 2a6a82570d4d24e5adb74c8ca323de8e1c33e82c Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Wed, 8 Jun 2022 13:34:45 -0400 Subject: [PATCH 006/108] Trim trailing whitespace --- src/editable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editable.ts b/src/editable.ts index 0cfa2adf..f3a217a6 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -54,7 +54,7 @@ const addCustomStyles = (container: HTMLElement): void => { } text { user-select: none; - } + } .copying { cursor: copy; } From 623a977ec61debf8346853f880c1b0eb62ab73bd Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Wed, 8 Jun 2022 16:55:06 -0400 Subject: [PATCH 007/108] Add license --- src/editable.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/editable.ts b/src/editable.ts index f3a217a6..0b530672 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + import { Operation } from './circuit'; import { leftPadding } from './constants'; import { box } from './formatters/formatUtils'; From 6f560124d65bc2f8f1a8aa8a9bd112e68b063a25 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Wed, 8 Jun 2022 17:25:37 -0400 Subject: [PATCH 008/108] Fix + add tests to getWireElemY --- __tests__/editable.test.ts | 32 ++++++++++++++++++++++++++++++++ src/editable.ts | 6 ++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index 34dd2be2..d23f7a4d 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -12,6 +12,7 @@ const { insertAfter, getDropzonePosition, getWireElemText, + getWireElemY, } = exportedForTesting; // Utlities @@ -359,3 +360,34 @@ describe('Testing getWireElementText', () => { expect(getWireElemText(groupElem)).toEqual('q1'); }); }); + +describe('Testing getWireElemY', () => { + let svgElem: SVGElement; + let groupElem: SVGGElement; + let lineElem: SVGGElement; + beforeEach(() => { + svgElem = document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGElement; + groupElem = document.createElementNS('http://www.w3.org/2000/svg', 'g') as SVGGElement; + lineElem = document.createElementNS('http://www.w3.org/2000/svg', 'line') as SVGLineElement; + groupElem.append(lineElem); + svgElem.append(groupElem); + }); + test('line element not exists', () => { + lineElem.remove(); + expect(() => getWireElemY(groupElem)).toThrowError('y not found'); + }); + test('get y element without y value', () => { + expect(() => getWireElemY(groupElem)).toThrowError('y not found'); + }); + test('get text element empty textContent', () => { + expect(() => getWireElemY(groupElem)).toThrowError('y not found'); + }); + test('should return 40', () => { + lineElem.setAttribute('y1', '40'); + expect(getWireElemY(groupElem)).toEqual(40); + }); + test('should return 99', () => { + lineElem.setAttribute('y1', '99'); + expect(getWireElemY(groupElem)).toEqual(99); + }); +}); diff --git a/src/editable.ts b/src/editable.ts index 0b530672..0a72b6e2 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -264,9 +264,10 @@ const getWireElemsY = (container: HTMLElement): Wires => { const getWireElemY = (wireElem: SVGGElement): number => { const lineElem = wireElem.querySelector('line'); - if (lineElem == null || lineElem.y1.baseVal.value == null) throw Error('y not found'); - return lineElem.y1.baseVal.value; + if (lineElem == null || lineElem.getAttribute('y1') == null) throw Error('y not found'); + return Number(lineElem.getAttribute('y1')); }; + const getWireElemText = (wireElem: SVGGElement): string => { const textElem = wireElem.querySelector('text'); if (textElem == null || textElem.textContent == null || textElem.textContent === '') @@ -323,6 +324,7 @@ const exportedForTesting = { insertAfter, getDropzonePosition, getWireElemText, + getWireElemY, }; export { addEditable, exportedForTesting }; From 2a06b9555bf4f5ccf55d45fed94f827000768493 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Wed, 8 Jun 2022 19:10:18 -0400 Subject: [PATCH 009/108] Add tests for getGate + getParent --- __tests__/editable.test.ts | 358 +++++++++++++++++++++++++++++++++++++ src/editable.ts | 2 + 2 files changed, 360 insertions(+) diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index d23f7a4d..46b0091f 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -13,6 +13,8 @@ const { getDropzonePosition, getWireElemText, getWireElemY, + getGate, + getParent, } = exportedForTesting; // Utlities @@ -391,3 +393,359 @@ describe('Testing getWireElemY', () => { expect(getWireElemY(groupElem)).toEqual(99); }); }); + +describe('Testing getParent', () => { + test('with level 0 gate', () => { + const operations = [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ]; + expect(getParent('0', operations)).toStrictEqual([ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ]); + }); + test('with level 1 gate', () => { + const operations = [ + { + gate: 'Foo', + conditionalRender: 3, + targets: [{ qId: 0 }, { qId: 1 }], + children: [ + { + gate: 'H', + targets: [{ qId: 1 }], + }, + { + gate: 'RX', + displayArgs: '(0.25)', + isControlled: true, + controls: [{ qId: 1 }], + targets: [{ qId: 0 }], + }, + ], + }, + { + gate: 'X', + targets: [{ qId: 3 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 1 }], + targets: [{ qId: 2 }, { qId: 3 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 2 }, { qId: 3 }], + targets: [{ qId: 1 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 1 }, { qId: 3 }], + targets: [{ qId: 2 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'measure', + isMeasurement: true, + controls: [{ qId: 0 }], + targets: [{ type: 1, qId: 0, cId: 0 }], + }, + { + gate: 'ApplyIfElseR', + isConditional: true, + controls: [{ type: 1, qId: 0, cId: 0 }], + targets: [], + children: [ + { + gate: 'H', + targets: [{ qId: 1 }], + conditionalRender: 1, + }, + { + gate: 'X', + targets: [{ qId: 1 }], + conditionalRender: 1, + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + conditionalRender: 2, + }, + { + gate: 'Foo', + targets: [{ qId: 3 }], + conditionalRender: 2, + }, + ], + }, + { + gate: 'SWAP', + targets: [{ qId: 0 }, { qId: 2 }], + children: [ + { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, + { gate: 'X', isControlled: true, controls: [{ qId: 2 }], targets: [{ qId: 0 }] }, + { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, + ], + }, + { + gate: 'ZZ', + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'ZZ', + targets: [{ qId: 0 }, { qId: 1 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 0 }, { qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + ]; + expect(getParent('0-1', operations)).toStrictEqual([ + { + gate: 'H', + targets: [{ qId: 1 }], + }, + { + gate: 'RX', + displayArgs: '(0.25)', + isControlled: true, + controls: [{ qId: 1 }], + targets: [{ qId: 0 }], + }, + ]); + }); +}); + +describe('Testing getGate', () => { + test('should return H gate', () => { + const operations = [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ]; + expect(getGate('0', operations)).toStrictEqual({ + gate: 'H', + targets: [{ qId: 0 }], + }); + }); + test('should return X gate', () => { + const operations = [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ]; + expect(getGate('1', operations)).toStrictEqual({ + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }); + }); + test('should return RX', () => { + const operations = [ + { + gate: 'Foo', + conditionalRender: 3, + targets: [{ qId: 0 }, { qId: 1 }], + children: [ + { + gate: 'H', + targets: [{ qId: 1 }], + }, + { + gate: 'RX', + displayArgs: '(0.25)', + isControlled: true, + controls: [{ qId: 1 }], + targets: [{ qId: 0 }], + }, + ], + }, + { + gate: 'X', + targets: [{ qId: 3 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 1 }], + targets: [{ qId: 2 }, { qId: 3 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 2 }, { qId: 3 }], + targets: [{ qId: 1 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 1 }, { qId: 3 }], + targets: [{ qId: 2 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'measure', + isMeasurement: true, + controls: [{ qId: 0 }], + targets: [{ type: 1, qId: 0, cId: 0 }], + }, + { + gate: 'ApplyIfElseR', + isConditional: true, + controls: [{ type: 1, qId: 0, cId: 0 }], + targets: [], + children: [ + { + gate: 'H', + targets: [{ qId: 1 }], + conditionalRender: 1, + }, + { + gate: 'X', + targets: [{ qId: 1 }], + conditionalRender: 1, + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + conditionalRender: 2, + }, + { + gate: 'Foo', + targets: [{ qId: 3 }], + conditionalRender: 2, + }, + ], + }, + { + gate: 'SWAP', + targets: [{ qId: 0 }, { qId: 2 }], + children: [ + { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, + { gate: 'X', isControlled: true, controls: [{ qId: 2 }], targets: [{ qId: 0 }] }, + { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, + ], + }, + { + gate: 'ZZ', + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'ZZ', + targets: [{ qId: 0 }, { qId: 1 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 0 }, { qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + ]; + expect(getGate('0-1', operations)).toStrictEqual({ + gate: 'RX', + displayArgs: '(0.25)', + isControlled: true, + controls: [{ qId: 1 }], + targets: [{ qId: 0 }], + }); + }); +}); diff --git a/src/editable.ts b/src/editable.ts index 0a72b6e2..8a55cd4a 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -325,6 +325,8 @@ const exportedForTesting = { getDropzonePosition, getWireElemText, getWireElemY, + getGate, + getParent, }; export { addEditable, exportedForTesting }; From 3e7dd43769a57b74e529bba8dc59cc0f80d557c3 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Wed, 8 Jun 2022 19:35:35 -0400 Subject: [PATCH 010/108] Add test for addCustomStyles --- __tests__/__snapshots__/editable.test.ts.snap | 35 ++++++++++++++ __tests__/editable.test.ts | 12 +++++ src/editable.ts | 47 +++++++++---------- 3 files changed, 70 insertions(+), 24 deletions(-) diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap index 637f61b6..7be5921b 100644 --- a/__tests__/__snapshots__/editable.test.ts.snap +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -1,5 +1,40 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Testing addCustomStyles verify css 1`] = ` +
+ +
+`; + exports[`Testing cursorCopy turn off copy cursor 1`] = `
{ }); }); }); + +describe('Testing addCustomStyles', () => { + test('verify css', () => { + const container = document.createElement('div'); + const style = document.createElement('style'); + container.append(style); + expect(container).toMatchSnapshot(); + addCustomStyles(container); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/editable.ts b/src/editable.ts index 8a55cd4a..bcc1d683 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -46,30 +46,28 @@ const addEditable = (container: HTMLElement, sqore: Sqore): void => { const addCustomStyles = (container: HTMLElement): void => { const style = container.querySelector('style'); if (style) { - style.innerHTML += ` - .dropzone { - fill: transparent; - stroke: transparent; - } - .dropzone:hover{ - fill: red; - opacity: 25%; - } - text { - user-select: none; - } - .copying { - cursor: copy; - } - .moving { - cursor: move; - } - .detail-panel { - display: flex; - align-content: center; - gap: 12px; - } - `; + style.innerHTML += `.dropzone { + fill: transparent; + stroke: transparent; + } + .dropzone:hover{ + fill: red; + opacity: 25%; + } + text { + user-select: none; + } + .copying { + cursor: copy; + } + .moving { + cursor: move; + } + .detail-panel { + display: flex; + align-content: center; + gap: 12px; + }`; } }; @@ -327,6 +325,7 @@ const exportedForTesting = { getWireElemY, getGate, getParent, + addCustomStyles, }; export { addEditable, exportedForTesting }; From b478d567ac1e2971cdc0d38ec8ee447ba57235c6 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Wed, 8 Jun 2022 19:41:00 -0400 Subject: [PATCH 011/108] Add test for addDocumentEvents --- __tests__/__snapshots__/editable.test.ts.snap | 4 ++++ __tests__/editable.test.ts | 11 +++++++++++ src/editable.ts | 1 + 3 files changed, 16 insertions(+) diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap index 7be5921b..9d3bfe1e 100644 --- a/__tests__/__snapshots__/editable.test.ts.snap +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -35,6 +35,10 @@ exports[`Testing addCustomStyles verify css 2`] = `
`; +exports[`Testing addDocumentEvents verify document events 1`] = `
`; + +exports[`Testing addDocumentEvents verify document events 2`] = `
`; + exports[`Testing cursorCopy turn off copy cursor 1`] = `
{ expect(container).toMatchSnapshot(); }); }); + +// Untestable +describe('Testing addDocumentEvents', () => { + test('verify document events', () => { + const container = document.createElement('div'); + expect(container).toMatchSnapshot(); + addDocumentEvents(container); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/editable.ts b/src/editable.ts index bcc1d683..6b5d1871 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -326,6 +326,7 @@ const exportedForTesting = { getGate, getParent, addCustomStyles, + addDocumentEvents, }; export { addEditable, exportedForTesting }; From 42fb70250ae4344d7948b2936e634ac671470fa9 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Thu, 9 Jun 2022 10:04:00 -0400 Subject: [PATCH 012/108] Add more tests --- __tests__/__snapshots__/editable.test.ts.snap | 251 ++++++++++++++++++ __tests__/editable.test.ts | 127 +++++++++ src/editable.ts | 22 +- 3 files changed, 395 insertions(+), 5 deletions(-) diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap index 9d3bfe1e..0d888290 100644 --- a/__tests__/__snapshots__/editable.test.ts.snap +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -39,6 +39,54 @@ exports[`Testing addDocumentEvents verify document events 1`] = `
`; exports[`Testing addDocumentEvents verify document events 2`] = `
`; +exports[`Testing createDropzone create dropzone on the left 1`] = ` + +`; + +exports[`Testing createDropzone create dropzone on the right 1`] = ` + +`; + +exports[`Testing createLeftDropzone create left dropzone 1`] = ` + +`; + +exports[`Testing createRightDropzone create dropzone right 1`] = ` + +`; + exports[`Testing cursorCopy turn off copy cursor 1`] = `
`; + +exports[`Testing getGateElems get 2 gates 1`] = ` +Array [ + + + + + + H + + + + , + + + + + + + , +] +`; + +exports[`Testing getGateElems get 3 gates 1`] = ` +Array [ + + + + + + H + + + + , + + + + + + + + + , + + + + + + + , +] +`; + +exports[`Testing getWireElems get 2 wires 1`] = ` +Array [ + + + + q0 + + , + + + + q1 + + , +] +`; + +exports[`Testing handleGateMouseDown copying, ctrlKey is true 1`] = ` +
+`; + +exports[`Testing handleGateMouseDown moving, ctrlKey is false 1`] = ` +
+`; diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index 22e249a3..f50dd1a0 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -1,6 +1,7 @@ import { Operation } from '../src/circuit'; import { exportedForTesting } from '../src/editable'; import { RegisterType } from '../src/register'; +import { draw, STYLES } from '../src/index'; const { getDataId, @@ -17,6 +18,12 @@ const { getParent, addCustomStyles, addDocumentEvents, + handleGateMouseDown, + getGateElems, + getWireElems, + createDropzone, + createLeftDropzone, + createRightDropzone, } = exportedForTesting; // Utlities @@ -772,3 +779,123 @@ describe('Testing addDocumentEvents', () => { expect(container).toMatchSnapshot(); }); }); + +describe('Testing handleGateMouseDown', () => { + test('copying, ctrlKey is true', () => { + const container = document.createElement('div'); + const ev = new MouseEvent('mousedown', { ctrlKey: true }); + handleGateMouseDown(ev, container); + expect(container).toMatchSnapshot(); + }); + test('moving, ctrlKey is false', () => { + const container = document.createElement('div'); + const ev = new MouseEvent('mousedown', { ctrlKey: false }); + handleGateMouseDown(ev, container); + expect(container).toMatchSnapshot(); + }); +}); + +describe('Testing getGateElems', () => { + test('get 2 gates', () => { + const container = document.createElement('div'); + const circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + draw(circuit, container, STYLES['default']); + const gateElems = getGateElems(container); + expect(gateElems).toHaveLength(2); + expect(gateElems).toMatchSnapshot(); + }); + test('get 3 gates', () => { + const container = document.createElement('div'); + const circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + draw(circuit, container, STYLES['default']); + const gateElems = getGateElems(container); + expect(gateElems).toHaveLength(3); + expect(gateElems).toMatchSnapshot(); + }); +}); + +describe('Testing getWireElems', () => { + test('get 2 wires', () => { + const container = document.createElement('div'); + const circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + draw(circuit, container, STYLES['default']); + const wireElems = getWireElems(container); + expect(wireElems).toHaveLength(2); + expect(wireElems).toMatchSnapshot(); + }); +}); + +describe('Testing createDropzone', () => { + test('create dropzone on the left', () => { + expect(createDropzone(0, 0, 20, 20, '0', 'left')).toMatchSnapshot(); + }); + test('create dropzone on the right', () => { + expect(createDropzone(0, 0, 20, 20, '0', 'right')).toMatchSnapshot(); + }); +}); + +describe('Testing createLeftDropzone', () => { + test('create left dropzone', () => { + expect(createLeftDropzone(0, 0, 20, '0')).toMatchSnapshot(); + }); +}); + +describe('Testing createRightDropzone', () => { + test('create dropzone right', () => { + expect(createRightDropzone(0, 0, 20, 20, '0')).toMatchSnapshot(); + }); +}); diff --git a/src/editable.ts b/src/editable.ts index 6b5d1871..2b395cca 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -210,11 +210,17 @@ const createDropzone = ( return dropzone; }; -const createLeftDropzone = (x: number, y: number, height: number, dataId: string): SVGElement => { - return createDropzone(x - leftPadding / 2, y, leftPadding / 2, height, dataId, 'left'); +const createLeftDropzone = (gateX: number, gateY: number, gateHeight: number, dataId: string): SVGElement => { + return createDropzone(gateX - leftPadding / 2, gateY, leftPadding / 2, gateHeight, dataId, 'left'); }; -const createRightDropzone = (x: number, y: number, width: number, height: number, dataId: string): SVGElement => { - return createDropzone(x + width, y, leftPadding / 2, height, dataId, 'right'); +const createRightDropzone = ( + gateX: number, + gateY: number, + gateWidth: number, + gateHeight: number, + dataId: string, +): SVGElement => { + return createDropzone(gateX + gateWidth, gateY, leftPadding / 2, gateHeight, dataId, 'right'); }; // Operation getters @@ -327,6 +333,12 @@ const exportedForTesting = { getParent, addCustomStyles, addDocumentEvents, + handleGateMouseDown, + getGateElems, + getWireElems, + createDropzone, + createLeftDropzone, + createRightDropzone, }; -export { addEditable, exportedForTesting }; +export { addEditable, exportedForTesting, getGateElems }; From be0685980bc658a86fa9813f73940abcf0bc6d5a Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Thu, 9 Jun 2022 10:23:32 -0400 Subject: [PATCH 013/108] add tests for getClosestWireY --- __tests__/editable.test.ts | 85 ++++++++++++++++++++++++++++++++++++++ src/editable.ts | 44 +++++++++++--------- 2 files changed, 110 insertions(+), 19 deletions(-) diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index f50dd1a0..7ea1e307 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -24,6 +24,7 @@ const { createDropzone, createLeftDropzone, createRightDropzone, + getClosestWireY, } = exportedForTesting; // Utlities @@ -899,3 +900,87 @@ describe('Testing createRightDropzone', () => { expect(createRightDropzone(0, 0, 20, 20, '0')).toMatchSnapshot(); }); }); + +describe('Testing getClosestWireY', () => { + test('should return 40', () => { + const container = document.createElement('div'); + const circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + const wires = { '40': 'q0', '100': 'q1' }; + draw(circuit, container, STYLES['default']); + expect(getClosestWireY(50, wires)).toEqual(40); + }); + test('should return 100', () => { + const container = document.createElement('div'); + const circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + const wires = { '40': 'q0', '100': 'q1' }; + draw(circuit, container, STYLES['default']); + expect(getClosestWireY(85, wires)).toEqual(100); + }); + test('should return null', () => { + const container = document.createElement('div'); + const circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + const wires = { '40': 'q0', '100': 'q1' }; + draw(circuit, container, STYLES['default']); + expect(getClosestWireY(120, wires)).toEqual(null); + }); +}); diff --git a/src/editable.ts b/src/editable.ts index 2b395cca..6b07be81 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -280,16 +280,15 @@ const getWireElemText = (wireElem: SVGGElement): string => { }; const getClosestWireY = (offsetY: number, wires: { [y: number]: string }): number | null => { - let wireY; - Object.entries(wires).forEach((wire) => { + for (const wire of Object.entries(wires)) { const y = wire[0]; const distance = Math.abs(offsetY - Number(y)); - // 15 is a magic number - if (distance < 15) { - wireY = y; + // 15 is an arbitrary number to determine the closeness of distance + if (distance <= 15) { + return Number(y); } - }); - return wireY || null; + } + return null; }; const getDropzonePosition = (element: SVGElement): string => { @@ -319,26 +318,33 @@ const cursorCopy = (container: HTMLElement, value: boolean): void => { }; const exportedForTesting = { - getDataId, - splitDataId, - cursorMove, - cursorCopy, - deleteAt, - insertBefore, - insertAfter, - getDropzonePosition, - getWireElemText, - getWireElemY, - getGate, - getParent, + // addEditable addCustomStyles, + // addDropzones addDocumentEvents, + // addDropzoneEvents + // addMouseEvents handleGateMouseDown, + // handleDropzoneMouseUp getGateElems, getWireElems, createDropzone, createLeftDropzone, createRightDropzone, + getParent, + getGate, + getDataId, + splitDataId, + // getWireElemsY + getWireElemY, + getWireElemText, + getClosestWireY, + getDropzonePosition, + insertBefore, + insertAfter, + deleteAt, + cursorMove, + cursorCopy, }; export { addEditable, exportedForTesting, getGateElems }; From 41bbf6897745bf2f871c741a7b0d1c21f71d457a Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Thu, 9 Jun 2022 10:47:08 -0400 Subject: [PATCH 014/108] Add tests for getWireElemsY + addDropzoneEvents --- __tests__/__snapshots__/editable.test.ts.snap | 26 ++ __tests__/editable.test.ts | 224 ++++++++++++++++++ src/editable.ts | 6 +- 3 files changed, 253 insertions(+), 3 deletions(-) diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap index 0d888290..34cc9398 100644 --- a/__tests__/__snapshots__/editable.test.ts.snap +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -39,6 +39,32 @@ exports[`Testing addDocumentEvents verify document events 1`] = `
`; exports[`Testing addDocumentEvents verify document events 2`] = `
`; +exports[`Testing addDropzoneEvents add 1 event 1`] = ` +
+ + + +
+`; + +exports[`Testing addDropzoneEvents add 1 event 2`] = ` +
+ + + + +
+`; + +exports[`Testing addDropzoneEvents add 2 events 1`] = ` +
+ + + + +
+`; + exports[`Testing createDropzone create dropzone on the left 1`] = ` { expect(getClosestWireY(120, wires)).toEqual(null); }); }); + +describe('test getWireElemsY', () => { + test('get 2 wires', () => { + const container = document.createElement('div'); + const circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + const expected = { '40': 'q0', '100': 'q1' }; + draw(circuit, container, STYLES['default']); + expect(getWireElemsY(container)).toStrictEqual(expected); + }); + test('get 4 wires', () => { + const container = document.createElement('div'); + const circuit = { + qubits: [{ id: 0, numChildren: 1 }, { id: 1 }, { id: 2 }, { id: 3 }], + operations: [ + { + gate: 'Foo', + conditionalRender: 3, + targets: [{ qId: 0 }, { qId: 1 }], + children: [ + { + gate: 'H', + targets: [{ qId: 1 }], + }, + { + gate: 'RX', + displayArgs: '(0.25)', + isControlled: true, + controls: [{ qId: 1 }], + targets: [{ qId: 0 }], + }, + ], + }, + { + gate: 'X', + targets: [{ qId: 3 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 1 }], + targets: [{ qId: 2 }, { qId: 3 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 2 }, { qId: 3 }], + targets: [{ qId: 1 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 1 }, { qId: 3 }], + targets: [{ qId: 2 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'measure', + isMeasurement: true, + controls: [{ qId: 0 }], + targets: [{ type: 1, qId: 0, cId: 0 }], + }, + { + gate: 'ApplyIfElseR', + isConditional: true, + controls: [{ type: 1, qId: 0, cId: 0 }], + targets: [], + children: [ + { + gate: 'H', + targets: [{ qId: 1 }], + conditionalRender: 1, + }, + { + gate: 'X', + targets: [{ qId: 1 }], + conditionalRender: 1, + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + conditionalRender: 2, + }, + { + gate: 'Foo', + targets: [{ qId: 3 }], + conditionalRender: 2, + }, + ], + }, + { + gate: 'SWAP', + targets: [{ qId: 0 }, { qId: 2 }], + children: [ + { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, + { gate: 'X', isControlled: true, controls: [{ qId: 2 }], targets: [{ qId: 0 }] }, + { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, + ], + }, + { + gate: 'ZZ', + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'ZZ', + targets: [{ qId: 0 }, { qId: 1 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 0 }, { qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + ], + }; + const expected = { '40': 'q0', '120': 'q1', '180': 'q2', '240': 'q3' }; + draw(circuit, container, STYLES['default']); + expect(getWireElemsY(container)).toStrictEqual(expected); + }); +}); + +describe('Testing addDropzoneEvents', () => { + interface Context { + container: HTMLElement; + operations: Operation[]; + wires: Wires; + renderFn: () => void; + } + + interface Wires { + [y: string]: string; + } + + test('add 1 event', () => { + const container = document.createElement('div'); + const svgElem = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + const dropzoneElem = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + svgElem.append(dropzoneElem); + container.append(svgElem); + interface Context { + container: HTMLElement; + operations: Operation[]; + wires: Wires; + renderFn: () => void; + } + + const context: Context = { + container: container, + operations: [], + wires: {}, + renderFn: () => { + return; + }, + }; + addDropzoneEvents(context); + expect(container).toMatchSnapshot(); + }); + test('add 2 events', () => { + const container = document.createElement('div'); + const svgElem = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + const dropzoneElem = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + const dropzoneElem1 = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + svgElem.append(dropzoneElem); + svgElem.append(dropzoneElem1); + container.append(svgElem); + interface Context { + container: HTMLElement; + operations: Operation[]; + wires: Wires; + renderFn: () => void; + } + + const context: Context = { + container: container, + operations: [], + wires: {}, + renderFn: () => { + return; + }, + }; + addDropzoneEvents(context); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/editable.ts b/src/editable.ts index 6b07be81..2e2a978f 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -322,7 +322,7 @@ const exportedForTesting = { addCustomStyles, // addDropzones addDocumentEvents, - // addDropzoneEvents + addDropzoneEvents, // addMouseEvents handleGateMouseDown, // handleDropzoneMouseUp @@ -335,7 +335,7 @@ const exportedForTesting = { getGate, getDataId, splitDataId, - // getWireElemsY + getWireElemsY, getWireElemY, getWireElemText, getClosestWireY, @@ -347,4 +347,4 @@ const exportedForTesting = { cursorCopy, }; -export { addEditable, exportedForTesting, getGateElems }; +export { addEditable, exportedForTesting }; From 6d9b5d58980a1aa3f0de42badb9beecc3ec2f7ec Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Thu, 9 Jun 2022 10:54:07 -0400 Subject: [PATCH 015/108] Add tests for addMouseEvents --- __tests__/__snapshots__/editable.test.ts.snap | 282 +++++++++++++++++- __tests__/editable.test.ts | 79 +++-- src/editable.ts | 2 +- 3 files changed, 337 insertions(+), 26 deletions(-) diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap index 34cc9398..00bc4da5 100644 --- a/__tests__/__snapshots__/editable.test.ts.snap +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -47,7 +47,7 @@ exports[`Testing addDropzoneEvents add 1 event 1`] = `
`; -exports[`Testing addDropzoneEvents add 1 event 2`] = ` +exports[`Testing addDropzoneEvents add 2 events 1`] = `
@@ -56,11 +56,283 @@ exports[`Testing addDropzoneEvents add 1 event 2`] = `
`; -exports[`Testing addDropzoneEvents add 2 events 1`] = ` +exports[`Testing addMouseEvents verify mouse events 1`] = `
- - - + + + + + |0⟩ + + + |0⟩ + + + + + + + q0 + + + + + + q1 + + + + + + + + + + + + + + + + H + + + + + + + + + + + +
`; diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index 2c09a384..9d9af652 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -4,29 +4,33 @@ import { RegisterType } from '../src/register'; import { draw, STYLES } from '../src/index'; const { - getDataId, - splitDataId, - cursorMove, - cursorCopy, - deleteAt, - insertBefore, - insertAfter, - getDropzonePosition, - getWireElemText, - getWireElemY, - getGate, - getParent, + // addEditable addCustomStyles, + // addDropzones addDocumentEvents, + addDropzoneEvents, + addMouseEvents, handleGateMouseDown, + // handleDropzoneMouseUp getGateElems, getWireElems, createDropzone, createLeftDropzone, createRightDropzone, - getClosestWireY, + getParent, + getGate, + getDataId, + splitDataId, getWireElemsY, - addDropzoneEvents, + getWireElemY, + getWireElemText, + getClosestWireY, + getDropzonePosition, + insertBefore, + insertAfter, + deleteAt, + cursorMove, + cursorCopy, } = exportedForTesting; // Utlities @@ -1163,12 +1167,6 @@ describe('Testing addDropzoneEvents', () => { const dropzoneElem = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); svgElem.append(dropzoneElem); container.append(svgElem); - interface Context { - container: HTMLElement; - operations: Operation[]; - wires: Wires; - renderFn: () => void; - } const context: Context = { container: container, @@ -1208,3 +1206,44 @@ describe('Testing addDropzoneEvents', () => { expect(container).toMatchSnapshot(); }); }); + +describe('Testing addMouseEvents', () => { + interface Context { + container: HTMLElement; + operations: Operation[]; + wires: Wires; + renderFn: () => void; + } + interface Wires { + [y: string]: string; + } + test('verify mouse events', () => { + const container = document.createElement('div'); + const circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + draw(circuit, container, STYLES['default']); + const context: Context = { + container: container, + operations: [], + wires: {}, + renderFn: () => { + return; + }, + }; + addMouseEvents(context); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/editable.ts b/src/editable.ts index 2e2a978f..687c799b 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -323,7 +323,7 @@ const exportedForTesting = { // addDropzones addDocumentEvents, addDropzoneEvents, - // addMouseEvents + addMouseEvents, handleGateMouseDown, // handleDropzoneMouseUp getGateElems, From 692250c3039deaed4c927bba27c0d01cce660860 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Thu, 9 Jun 2022 11:03:28 -0400 Subject: [PATCH 016/108] Remove svg id in snapshot --- __tests__/__snapshots__/editable.test.ts.snap | 1 - __tests__/editable.test.ts | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap index 00bc4da5..b6ae0cf6 100644 --- a/__tests__/__snapshots__/editable.test.ts.snap +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -61,7 +61,6 @@ exports[`Testing addMouseEvents verify mouse events 1`] = ` diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index 9d9af652..31c5cacb 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -1243,6 +1243,8 @@ describe('Testing addMouseEvents', () => { return; }, }; + const svgElem = container.querySelector('svg'); + if (svgElem != null) svgElem.removeAttribute('id'); addMouseEvents(context); expect(container).toMatchSnapshot(); }); From 0dc44fcd49f2194a811f08b56a0ea895f1ab56f2 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Thu, 9 Jun 2022 11:12:03 -0400 Subject: [PATCH 017/108] Trim trailing whitespace --- __tests__/__snapshots__/editable.test.ts.snap | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap index b6ae0cf6..51dcc84a 100644 --- a/__tests__/__snapshots__/editable.test.ts.snap +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -65,7 +65,7 @@ exports[`Testing addMouseEvents verify mouse events 1`] = ` width="140" >
`; +exports[`Testing addDropzones verify dropzones 1`] = ` +
+ + + + + |0⟩ + + + |0⟩ + + + + + + + q0 + + + + + + q1 + + + + + + + + + + + + + + + + H + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+`; + exports[`Testing addMouseEvents verify mouse events 1`] = `
{ expect(console.log).toHaveBeenCalledWith('callbackFn is triggered'); }); }); + +describe('Testing addDropzones', () => { + test('verify dropzones', () => { + Object.defineProperty(window.SVGElement.prototype, 'getBBox', { + writable: true, + value: () => ({ + x: 0, + y: 0, + width: 0, + height: 0, + }), + }); + const container = document.createElement('div'); + const circuit: Circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + draw(circuit, container, STYLES['default'], 0, true); + const svgElem = container.querySelector('svg'); + if (svgElem != null) svgElem.removeAttribute('id'); + addDropzones(container); + expect(container).toMatchSnapshot(); + }); +}); From 5620b601696210b185b1bcfe1b5cf699a35eff21 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Mon, 13 Jun 2022 11:58:23 -0400 Subject: [PATCH 023/108] Integrate editable styles in styles.ts --- __tests__/__snapshots__/editable.test.ts.snap | 102 ++++++++++++------ __tests__/editable.test.ts | 13 --- src/editable.ts | 30 ------ src/styles.ts | 48 ++++++++- 4 files changed, 114 insertions(+), 79 deletions(-) diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap index 634d04b7..1d834e89 100644 --- a/__tests__/__snapshots__/editable.test.ts.snap +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -1,40 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Testing addCustomStyles verify css 1`] = ` -
- -
-`; - exports[`Testing addDocumentEvents verify document events 1`] = `
`; exports[`Testing addDocumentEvents verify document events 2`] = `
`; @@ -191,6 +156,29 @@ exports[`Testing addDropzones verify dropzones 1`] = ` visibility: visible; opacity: 1; transition: opacity 1s; + } + + .dropzone { + fill: transparent; + stroke: transparent; + } + .dropzone:hover{ + fill: red; + opacity: 25%; + } + text { + user-select: none; + } + .copying { + cursor: copy; + } + .moving { + cursor: move; + } + .detail-panel { + display: flex; + align-content: center; + gap: 12px; }.dropzone { fill: transparent; stroke: transparent; @@ -641,6 +629,50 @@ exports[`Testing addMouseEvents verify mouse events 1`] = ` opacity: 1; transition: opacity 1s; } + + .dropzone { + fill: transparent; + stroke: transparent; + } + .dropzone:hover{ + fill: red; + opacity: 25%; + } + text { + user-select: none; + } + .copying { + cursor: copy; + } + .moving { + cursor: move; + } + .detail-panel { + display: flex; + align-content: center; + gap: 12px; + }.dropzone { + fill: transparent; + stroke: transparent; + } + .dropzone:hover{ + fill: red; + opacity: 25%; + } + text { + user-select: none; + } + .copying { + cursor: copy; + } + .moving { + cursor: move; + } + .detail-panel { + display: flex; + align-content: center; + gap: 12px; + } { }); }); -describe('Testing addCustomStyles', () => { - test('verify css', () => { - const container = document.createElement('div'); - const style = document.createElement('style'); - container.append(style); - expect(container).toMatchSnapshot(); - addCustomStyles(container); - expect(container).toMatchSnapshot(); - }); -}); - -// Untestable describe('Testing addDocumentEvents', () => { test('verify document events', () => { const container = document.createElement('div'); diff --git a/src/editable.ts b/src/editable.ts index c78b28c2..55fd9fe8 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -35,7 +35,6 @@ const addEditable = (container: HTMLElement, sqore: Sqore, callbackFn?: () => vo wires: getWireElemsY(container), renderFn: getRenderFn(container, sqore, callbackFn), }; - addCustomStyles(container); addDropzones(container); addDocumentEvents(container); addDropzoneEvents(context); @@ -44,34 +43,6 @@ const addEditable = (container: HTMLElement, sqore: Sqore, callbackFn?: () => vo // Commands -const addCustomStyles = (container: HTMLElement): void => { - const style = container.querySelector('style'); - if (style) { - style.innerHTML += `.dropzone { - fill: transparent; - stroke: transparent; - } - .dropzone:hover{ - fill: red; - opacity: 25%; - } - text { - user-select: none; - } - .copying { - cursor: copy; - } - .moving { - cursor: move; - } - .detail-panel { - display: flex; - align-content: center; - gap: 12px; - }`; - } -}; - const addDropzones = (container: HTMLElement): void => { const gateElems = getGateElems(container); gateElems.forEach((gateElem) => { @@ -326,7 +297,6 @@ const cursorCopy = (container: HTMLElement, value: boolean): void => { const exportedForTesting = { // addEditable - addCustomStyles, addDropzones, addDocumentEvents, addDropzoneEvents, diff --git a/src/styles.ts b/src/styles.ts index 491d563e..d14434c9 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -95,7 +95,8 @@ export const style = (customStyle: StyleConfig = {}): string => { return `${_defaultGates(styleConfig)} ${_classicallyControlledGates(styleConfig)} - ${_expandCollapse}`; + ${_expandCollapse} + ${_editable}`; }; const _defaultGates = (styleConfig: StyleConfig): string => ` @@ -234,3 +235,48 @@ const _expandCollapse = ` opacity: 1; transition: opacity 1s; }`; + +const _editable = ` + .dropzone { + fill: transparent; + stroke: transparent; + } + .dropzone:hover{ + fill: red; + opacity: 25%; + } + text { + user-select: none; + } + .copying { + cursor: copy; + } + .moving { + cursor: move; + } + .detail-panel { + display: flex; + align-content: center; + gap: 12px; + }.dropzone { + fill: transparent; + stroke: transparent; + } + .dropzone:hover{ + fill: red; + opacity: 25%; + } + text { + user-select: none; + } + .copying { + cursor: copy; + } + .moving { + cursor: move; + } + .detail-panel { + display: flex; + align-content: center; + gap: 12px; + }`; From 2c6be0656069dec9f644fc8d2233ef0b7ae2c126 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Mon, 13 Jun 2022 12:17:32 -0400 Subject: [PATCH 024/108] Display grab cursor style when hover over gates --- __tests__/__snapshots__/editable.test.ts.snap | 14 ++++++++++++++ src/styles.ts | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap index 1d834e89..d8e2ae43 100644 --- a/__tests__/__snapshots__/editable.test.ts.snap +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -201,6 +201,13 @@ exports[`Testing addDropzones verify dropzones 1`] = ` align-content: center; gap: 12px; } + g.gate:not([data-expanded=true]) rect { + cursor: grab; + } + g.gate:not([data-expanded=true]) circle { + cursor: grab; + } + Date: Mon, 13 Jun 2022 12:33:07 -0400 Subject: [PATCH 025/108] Rename callbackFn to onCircuitChange --- __tests__/editable.test.ts | 8 ++++---- example/script.js | 2 +- src/editable.ts | 16 ++++++++-------- src/index.ts | 6 +++--- src/sqore.ts | 26 +++++++++++++------------- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index 3729db60..dc5651cf 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -1240,7 +1240,7 @@ describe('Testing addMouseEvents', () => { }); describe('Testing getRenderFn', () => { - test('check console.log displaying "callbackFn is triggered"', () => { + test('check console.log displaying "onCircuitChange is triggered"', () => { Object.defineProperty(window.SVGElement.prototype, 'getBBox', { writable: true, value: () => ({ @@ -1273,12 +1273,12 @@ describe('Testing getRenderFn', () => { ], }; const sqore = new Sqore(circuit, STYLES['default']); - const callbackFn = () => console.log('callbackFn is triggered'); - const renderFn = getRenderFn(container, sqore, callbackFn); + const onCircuitChange = () => console.log('onCircuitChange is triggered'); + const renderFn = getRenderFn(container, sqore, onCircuitChange); jest.spyOn(console, 'log'); renderFn(); - expect(console.log).toHaveBeenCalledWith('callbackFn is triggered'); + expect(console.log).toHaveBeenCalledWith('onCircuitChange is triggered'); }); }); diff --git a/example/script.js b/example/script.js index ef3ef492..5c1aa464 100644 --- a/example/script.js +++ b/example/script.js @@ -12,7 +12,7 @@ if (typeof qviz != 'undefined') { const sampleDiv = document.getElementById('sample'); if (sampleDiv != null) { - qviz.draw(sample, sampleDiv, qviz.STYLES['Default'], 0, true, () => console.log('callbackFn')); + qviz.draw(sample, sampleDiv, qviz.STYLES['Default'], 0, true, () => console.log('onCircuitChange')); } const teleportDiv = document.getElementById('teleport'); diff --git a/src/editable.ts b/src/editable.ts index 55fd9fe8..e0407fb6 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -22,18 +22,18 @@ let _sourceTarget: SVGElement | null; /** * Add editable elements and events. * - * @param container HTML element for rendering visualization into. - * @param sqore Sqore object - * @param callbackFn User-provided callback function triggered when circuit is changed + * @param container HTML element for rendering visualization into. + * @param sqore Sqore object + * @param onCircuitChange User-provided callback function triggered when circuit is changed * */ -const addEditable = (container: HTMLElement, sqore: Sqore, callbackFn?: () => void): void => { +const addEditable = (container: HTMLElement, sqore: Sqore, onCircuitChange?: () => void): void => { const context: Context = { container: container, operations: sqore.circuit.operations, wires: getWireElemsY(container), - renderFn: getRenderFn(container, sqore, callbackFn), + renderFn: getRenderFn(container, sqore, onCircuitChange), }; addDropzones(container); addDocumentEvents(container); @@ -220,10 +220,10 @@ const getGate = (dataId: string, operations: Operation[]): Operation => { }; // Utilities -const getRenderFn = (container: HTMLElement, sqore: Sqore, callbackFn?: () => void): (() => void) => { +const getRenderFn = (container: HTMLElement, sqore: Sqore, onCircuitChange?: () => void): (() => void) => { return () => { - sqore.draw(container, 0, true, callbackFn); - if (callbackFn) callbackFn(); + sqore.draw(container, 0, true, onCircuitChange); + if (onCircuitChange) onCircuitChange(); }; }; diff --git a/src/index.ts b/src/index.ts index 49973b3f..5e5d770f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,7 @@ import { StyleConfig } from './styles'; * @param style Custom visualization style. * @param renderDepth Initial layer depth at which to render gates. * @param isEditable Optional value enabling/disabling editable feature - * @param callbackFn Optional function to trigger when changing elements in circuit + * @param onCircuitChange Optional function to trigger when changing elements in circuit */ export const draw = ( circuit: Circuit, @@ -21,11 +21,11 @@ export const draw = ( style: StyleConfig | string = {}, renderDepth = 0, isEditable?: boolean, - callbackFn?: () => void, + onCircuitChange?: () => void, ): void => { const sqore = new Sqore(circuit, style); - sqore.draw(container, renderDepth, isEditable, callbackFn); + sqore.draw(container, renderDepth, isEditable, onCircuitChange); }; export { STYLES } from './styles'; diff --git a/src/sqore.ts b/src/sqore.ts index 990ea3dc..878eef24 100644 --- a/src/sqore.ts +++ b/src/sqore.ts @@ -57,9 +57,9 @@ export class Sqore { * @param container HTML element for rendering visualization into. * @param renderDepth Initial layer depth at which to render gates. * @param isEditable Optional value enabling/disabling editable feature - * @param callbackFn Optional function to trigger when changing elements in circuit + * @param onCircuitChange Optional function to trigger when changing elements in circuit */ - draw(container: HTMLElement, renderDepth = 0, isEditable?: boolean, callbackFn?: () => void): void { + draw(container: HTMLElement, renderDepth = 0, isEditable?: boolean, onCircuitChange?: () => void): void { // Inject into container if (container == null) throw new Error(`Container not provided.`); @@ -81,7 +81,7 @@ export class Sqore { const id: string = circuit.operations[0].dataAttributes['id']; this.expandOperation(circuit.operations, id); } - this.renderCircuit(container, circuit, isEditable, callbackFn); + this.renderCircuit(container, circuit, isEditable, onCircuitChange); } /** @@ -109,23 +109,23 @@ export class Sqore { * @param container HTML element for rendering visualization into. * @param circuit Circuit object to be rendered. * @param isEditable Optional value enabling/disabling editable feature - * @param callbackFn Optional function to trigger when changing elements in circuit + * @param onCircuitChange Optional function to trigger when changing elements in circuit */ private renderCircuit( container: HTMLElement, circuit: Circuit, isEditable?: boolean, - callbackFn?: () => void, + onCircuitChange?: () => void, ): void { // Create visualization components const composedSqore: ComposedSqore = this.compose(circuit); const svg: SVGElement = this.generateSvg(composedSqore); container.innerHTML = ''; container.appendChild(svg); - this.addGateClickHandlers(container, circuit, isEditable, callbackFn); + this.addGateClickHandlers(container, circuit, isEditable, onCircuitChange); if (isEditable) { - addEditable(container, this, callbackFn); + addEditable(container, this, onCircuitChange); } } @@ -243,17 +243,17 @@ export class Sqore { * @param container HTML element containing visualized circuit. * @param circuit Circuit to be visualized. * @param isEditable Optional value enabling/disabling editable feature - * @param callbackFn Optional function to trigger when changing elements in circuit + * @param onCircuitChange Optional function to trigger when changing elements in circuit * */ private addGateClickHandlers( container: HTMLElement, circuit: Circuit, isEditable?: boolean, - callbackFn?: () => void, + onCircuitChange?: () => void, ): void { this.addClassicalControlHandlers(container); - this.addZoomHandlers(container, circuit, isEditable, callbackFn); + this.addZoomHandlers(container, circuit, isEditable, onCircuitChange); } /** @@ -310,14 +310,14 @@ export class Sqore { * @param container HTML element containing visualized circuit. * @param circuit Circuit to be visualized. * @param isEditable Optional value enabling/disabling editable feature - * @param callbackFn Optional function to trigger when changing elements in circuit + * @param onCircuitChange Optional function to trigger when changing elements in circuit * */ private addZoomHandlers( container: HTMLElement, circuit: Circuit, isEditable?: boolean, - callbackFn?: () => void, + onCircuitChange?: () => void, ): void { container.querySelectorAll('.gate .gate-control').forEach((ctrl) => { // Zoom in on clicked gate @@ -329,7 +329,7 @@ export class Sqore { } else if (ctrl.classList.contains('gate-expand')) { this.expandOperation(circuit.operations, gateId); } - this.renderCircuit(container, circuit, isEditable, callbackFn); + this.renderCircuit(container, circuit, isEditable, onCircuitChange); ev.stopPropagation(); } }); From b46060bb1695d3f455cd312dd0dd9cbd2fcb7968 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Mon, 13 Jun 2022 12:44:48 -0400 Subject: [PATCH 026/108] Document instructions on Editable feature --- example/script.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/example/script.js b/example/script.js index 5c1aa464..76149998 100644 --- a/example/script.js +++ b/example/script.js @@ -12,7 +12,12 @@ if (typeof qviz != 'undefined') { const sampleDiv = document.getElementById('sample'); if (sampleDiv != null) { - qviz.draw(sample, sampleDiv, qviz.STYLES['Default'], 0, true, () => console.log('onCircuitChange')); + const isEditable = true; + const onCircuitChange = () => console.log('onCircuitChange triggered'); + /* Pass in isEditable = true to allow circuit to be editable */ + /* Pass in onCircuitChange to trigger callback function + whenever there is a change in circuit */ + qviz.draw(sample, sampleDiv, qviz.STYLES['Default'], 0, isEditable, onCircuitChange); } const teleportDiv = document.getElementById('teleport'); From 4bfea117a9b6e653bc5472ae4f0a9e698e8e5c97 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Mon, 13 Jun 2022 12:47:46 -0400 Subject: [PATCH 027/108] Add license --- __tests__/editable.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index dc5651cf..50e7762d 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + import { Circuit, Operation } from '../src/circuit'; import { exportedForTesting } from '../src/editable'; import { RegisterType } from '../src/register'; From e41f5255738d2bf471497c258c5b779712904bb7 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Mon, 13 Jun 2022 12:51:58 -0400 Subject: [PATCH 028/108] Only allow scrollbar if neccessary --- example/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/index.html b/example/index.html index c3060fdd..e612d7b8 100644 --- a/example/index.html +++ b/example/index.html @@ -28,7 +28,7 @@ } .container { - overflow: scroll; + overflow: auto; } From 8e0c9e6dedaebed135f82c5b9fbb5ab71a95eacf Mon Sep 17 00:00:00 2001 From: Aaron Nguyen <37777232+aaronthangnguyen@users.noreply.github.com> Date: Tue, 28 Jun 2022 12:43:09 -0400 Subject: [PATCH 029/108] Reimplement adding dropzones (#69) * Hardcode dropzone-layer * Create basic functionalities * Add identifiable class to x-dot * Add data-gate-id & data-gate-wire * Extract _getCenter * Add events * Change function names * Add move & copy * Fix edge cases * Add offset recursively on wires * Hold Ctrl to copy * Extra stuffs * Let onCircuitChange taking in circuit object * Add partial support for group gates * Remove unnecessary function --- example/script.js | 9 +- src/editable.ts | 540 ++++++++++++++++++-------------- src/formatters/gateFormatter.ts | 2 +- src/index.ts | 2 +- src/sqore.ts | 25 +- src/styles.ts | 52 +-- 6 files changed, 334 insertions(+), 296 deletions(-) diff --git a/example/script.js b/example/script.js index 76149998..9c15533c 100644 --- a/example/script.js +++ b/example/script.js @@ -7,13 +7,16 @@ if (typeof qviz != 'undefined') { /* These examples shows how to draw circuits into containers. */ const entangleDiv = document.getElementById('entangle'); if (entangleDiv != null) { - qviz.draw(entangle, entangleDiv, qviz.STYLES['Default']); + qviz.draw(entangle, entangleDiv, qviz.STYLES['Default'], 0, true); } const sampleDiv = document.getElementById('sample'); if (sampleDiv != null) { const isEditable = true; - const onCircuitChange = () => console.log('onCircuitChange triggered'); + const onCircuitChange = (circuit) => { + console.log('New circuit ↓'); + console.log(circuit); + }; /* Pass in isEditable = true to allow circuit to be editable */ /* Pass in onCircuitChange to trigger callback function whenever there is a change in circuit */ @@ -22,7 +25,7 @@ if (typeof qviz != 'undefined') { const teleportDiv = document.getElementById('teleport'); if (teleportDiv != null) { - qviz.draw(teleport, teleportDiv, qviz.STYLES['Default']); + qviz.draw(teleport, teleportDiv, qviz.STYLES['Default'], 0, true); } const groverDiv = document.getElementById('grover'); diff --git a/src/editable.ts b/src/editable.ts index e0407fb6..befbf36f 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -1,24 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { Operation } from './circuit'; -import { leftPadding } from './constants'; +import { Circuit, Operation } from './circuit'; import { box } from './formatters/formatUtils'; import { Sqore } from './sqore'; interface Context { container: HTMLElement; + svg: SVGElement; operations: Operation[]; - wires: Wires; + wireData: number[]; renderFn: () => void; + paddingY: number; + selectedId: string | null; + selectedWire: string | null; } -interface Wires { - [y: string]: string; -} - -let _sourceTarget: SVGElement | null; - /** * Add editable elements and events. * @@ -28,301 +25,368 @@ let _sourceTarget: SVGElement | null; * */ -const addEditable = (container: HTMLElement, sqore: Sqore, onCircuitChange?: () => void): void => { +const addEditable = (container: HTMLElement, sqore: Sqore, onCircuitChange?: (circuit: Circuit) => void): void => { + const svg = container.querySelector('svg') as SVGElement; + const context: Context = { container: container, + svg, operations: sqore.circuit.operations, - wires: getWireElemsY(container), - renderFn: getRenderFn(container, sqore, onCircuitChange), + wireData: _wireData(container), + renderFn: _renderFn(container, sqore, onCircuitChange), + paddingY: 20, + selectedId: null, + selectedWire: null, }; - addDropzones(container); - addDocumentEvents(container); - addDropzoneEvents(context); - addMouseEvents(context); -}; -// Commands - -const addDropzones = (container: HTMLElement): void => { - const gateElems = getGateElems(container); - gateElems.forEach((gateElem) => { - const { x, y, width, height } = gateElem.getBBox(); - const dataId = getDataId(gateElem); - gateElem.append(createLeftDropzone(x, y, height, dataId)); - gateElem.append(createRightDropzone(x, y, width, height, dataId)); - }); + // addDropzones(container); + // addDocumentEvents(container); + _addDataWires(container); + svg.appendChild(_dropzoneLayer(context)); + // addDropzoneEvents(context); + // addMouseEvents(context); + _addEvents(context); }; -const addDocumentEvents = (container: HTMLElement): void => { - container.addEventListener('click', (ev: MouseEvent) => { - _sourceTarget = null; - if (ev.ctrlKey) return; - }); - container.addEventListener('contextmenu', (ev: MouseEvent) => { - ev.preventDefault(); - }); - container.addEventListener('mouseup', () => { - cursorCopy(container, false); - cursorMove(container, false); +const _addDataWires = (container: HTMLElement) => { + const elems = _hostElems(container); + elems.forEach((elem) => { + const { cY } = _center(elem); + // i.e. cY = 40, wireData returns [40, 100, 140, 180] + // dataWire will return 0, which is the index of 40 in wireData + const dataWire = _wireData(container).findIndex((y) => y === cY); + if (dataWire !== -1) { + elem.setAttribute('data-wire', `${dataWire}`); + } else { + const { y, height } = elem.getBBox(); + const wireData = _wireData(container); + const groupDataWire = wireData.findIndex((wireY) => wireY > y && wireY < y + height); + elem.setAttribute('data-wire', `${groupDataWire}`); + } }); }; -const addDropzoneEvents = (context: Context): void => { - const { container } = context; - const dropzoneElems = container.querySelectorAll('.dropzone'); - dropzoneElems.forEach((dropzoneElem) => { - dropzoneElem.addEventListener('mouseup', (ev: MouseEvent) => handleDropzoneMouseUp(ev, context)); - }); +/** + * Create a list of wires that element is spanning on. + * i.e. Gate 'Foo' spans on wire 0 (y=40), 1 (y=100), and 2 (y=140). + * Function returns [40, 100, 140] + */ +const _elemWires = (elem: SVGGraphicsElement, wireData: number[]) => { + const { y, height } = elem.getBBox(); + return wireData.filter((wireY) => wireY > y && wireY < y + height); }; -const addMouseEvents = (context: Context): void => { - const { container } = context; - const gateElems = getGateElems(container); - gateElems.forEach((gateElem) => { - gateElem.addEventListener('mousedown', (ev: MouseEvent) => handleGateMouseDown(ev, container)); - }); +const _hostElems = (container: HTMLElement) => { + return container.querySelectorAll( + '[class^="gate-"]:not(.gate-control, .gate-swap), .control-dot, .oplus, .cross', + ); }; -// Event handlers -const handleGateMouseDown = (ev: MouseEvent, container: HTMLElement): void => { - ev.stopPropagation(); - _sourceTarget = ev.currentTarget as SVGGElement; +const _wirePrefixes = (wireData: number[]) => wireData.map((wireY, index) => ({ index, wireY, prefixX: 40 })); - // Ctrl + Mousedown to copy. Mousedown only to move. - ev.ctrlKey ? cursorCopy(container, true) : cursorMove(container, true); +/** + * Find center point of element + */ +const _center = (elem: SVGGraphicsElement) => { + const { x, y, width, height } = elem.getBBox(); + return { cX: x + width / 2, cY: y + height / 2 }; }; -const handleDropzoneMouseUp = (ev: MouseEvent, context: Context): void | false => { - ev.stopPropagation(); - - const { container, operations, wires, renderFn } = context; - - const currentTarget = ev.currentTarget as SVGGElement; - - if (!currentTarget) return false; - - const dataId = getDataId(currentTarget); - const parent = getParent(dataId, operations); - const index = splitDataId(dataId).pop(); - const position = getDropzonePosition(currentTarget); - - if (_sourceTarget == null) return false; - - const sourceDataId = getDataId(_sourceTarget); - const sourceParent = getParent(sourceDataId, operations); - const sourceIndex = splitDataId(sourceDataId).pop(); - - if (index == null || sourceIndex == null) return false; - - const newGate = getGate(sourceDataId, operations); - const wireY = getClosestWireY(ev.offsetY, wires); - - // Not allow Measure gate to move vertically - if (wireY != null && newGate.gate !== 'measure') { - // wires[wireY] returns qubit name (i.e: 'q0') - // this remove 'q' and assign an index (i.e: 0) - const index = Number(wires[wireY].slice(1)); - const [firstTarget, ...targetsExceptFirst] = newGate.targets; - // Reserve all other properties, only change qId - Object.assign(firstTarget, { ...firstTarget, qId: index }); - // Reserve all other targets, only change first target - Object.assign(newGate, { ...newGate, targets: [firstTarget, ...targetsExceptFirst] }); - } +/** + * Create dropzone layer with all dropzones popullated + */ +const _dropzoneLayer = (context: Context) => { + const dropzoneLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + dropzoneLayer.classList.add('dropzone-layer'); - // Remove source element if moving using Ctrl + Mousedown - if (!ev.ctrlKey) { - deleteAt(sourceParent, sourceIndex); - } + const { container, svg, wireData, operations, paddingY } = context; + const elems = _hostElems(container); - // If dropzone is left of gate, insert before gate. - // Otherwise, insert after. - if (position === 'left') { - insertBefore(parent, index, newGate); - } else { - insertAfter(parent, index, newGate); - } + const wirePrefixes = _wirePrefixes(wireData); - // Remove cursor styles - cursorCopy(container, false); - cursorMove(container, false); + // Sort host elements by its x property + const sortedElems = Array.from(elems).sort((first, second) => { + const { x: x1 } = first.getBBox(); + const { x: x2 } = second.getBBox(); + return x1 - x2; + }); - // Redraw the circuit - renderFn(); -}; + // Add dropzones for each host elements + sortedElems.map((elem) => { + const { cX, cY } = _center(elem); + const wirePrefix = wirePrefixes.find((item) => item.wireY === cY); + + // Check to prevent group gates creating dropzones between wires + if (wirePrefix) { + const prefixX = wirePrefix.prefixX; + const elemDropzone = box(prefixX, cY - paddingY, cX - prefixX, paddingY * 2, 'dropzone'); + elemDropzone.setAttribute('data-dropzone-id', _equivDataId(elem) || ''); + elemDropzone.setAttribute('data-dropzone-wire', `${wirePrefix.index}`); + + wirePrefix.prefixX = cX; + + dropzoneLayer.appendChild(elemDropzone); + } else { + // Let group gates creating dropzones for each wire + const { x } = elem.getBBox(); + const elemWires = _elemWires(elem, wireData); + + elemWires.map((wireY) => { + const wirePrefix = wirePrefixes.find((item) => item.wireY === wireY); + if (wirePrefix) { + const prefixX = wirePrefix.prefixX; + const elemDropzone = box(prefixX, wireY - paddingY, x - prefixX, paddingY * 2, 'dropzone'); + elemDropzone.setAttribute('data-dropzone-id', _equivDataId(elem) || ''); + elemDropzone.setAttribute('data-dropzone-wire', `${wirePrefix.index}`); + + wirePrefix.prefixX = x; + + dropzoneLayer.appendChild(elemDropzone); + } + }); + console.log({ elem, x, elemWires }); + } + }); -// Element getters + // Add remaining dropzones to fit max-width of the circuit + wirePrefixes.map(({ wireY, prefixX }) => { + const maxWidth = Number(svg.getAttribute('width')); + const elemDropzone = box(prefixX, wireY - paddingY, maxWidth - prefixX, paddingY * 2, 'dropzone'); + elemDropzone.setAttribute('data-dropzone-id', `${operations.length}`); + const index = wireData.findIndex((item) => item === wireY); + elemDropzone.setAttribute('data-dropzone-wire', `${index}`); + dropzoneLayer.appendChild(elemDropzone); + }); -const getGateElems = (container: HTMLElement): SVGGElement[] => { - return Array.from(container.querySelectorAll('g.gate')); + return dropzoneLayer; }; -const getWireElems = (container: HTMLElement): SVGGElement[] => { +const _wireData = (container: HTMLElement) => { // elems include qubit wires and lines of measure gates const elems = container.querySelectorAll('svg > g:nth-child(3) > g'); // filter out elements having more than 2 elements because // qubit wires contain only 2 elements: and // lines of measure gates contain 4 elements - return Array.from(elems).filter((elem) => elem.childElementCount < 3); + const wireElems = Array.from(elems).filter((elem) => elem.childElementCount < 3); + const wireData = wireElems.map((wireElem) => { + const lineElem = wireElem.children[0] as SVGLineElement; + return lineElem.y1.baseVal.value; + }); + return wireData; }; -// Element creators - -const createDropzone = ( - x: number, - y: number, - width: number, - height: number, - dataId: string, - position: 'left' | 'right', -): SVGElement => { - const dropzone = box(x, y, width, height, `dropzone`); - dropzone.setAttribute('data-id', dataId); - dropzone.setAttribute('data-dropzone-position', position); - return dropzone; +/** + * Find equivalent gate element of host element + */ +const _equivGateElem = (elem: SVGElement) => { + return elem.closest('[data-id]'); }; -const createLeftDropzone = (gateX: number, gateY: number, gateHeight: number, dataId: string): SVGElement => { - return createDropzone(gateX - leftPadding / 2, gateY, leftPadding / 2, gateHeight, dataId, 'left'); +/** + * Find data-id of host element + */ +const _equivDataId = (elem: SVGElement) => { + const gateElem = _equivGateElem(elem); + return gateElem != null ? gateElem.getAttribute('data-id') : null; }; -const createRightDropzone = ( - gateX: number, - gateY: number, - gateWidth: number, - gateHeight: number, - dataId: string, -): SVGElement => { - return createDropzone(gateX + gateWidth, gateY, leftPadding / 2, gateHeight, dataId, 'right'); + +/** + * Disable contextmenu default behaviors + */ +const _addContextMenuEvents = (container: HTMLElement) => { + container.addEventListener('contextmenu', (ev: MouseEvent) => { + ev.preventDefault(); + }); }; -// Operation getters +/** + * Add events specifically for dropzoneLayer + */ +const _addDropzoneLayerEvents = (container: HTMLElement, dropzoneLayer: SVGGElement) => { + container.addEventListener('mouseup', () => (dropzoneLayer.style.display = 'none')); +}; -const getParent = (dataId: string, operations: Operation[]): Operation[] => { - const segments = splitDataId(dataId); - // Remove last segment to navigate to parent instead of child - segments.pop(); +const _addEvents = (context: Context) => { + const { container, operations, renderFn } = context; + const dropzoneLayer = container.querySelector('.dropzone-layer') as SVGGElement; + + _addContextMenuEvents(container); + _addDropzoneLayerEvents(container, dropzoneLayer); + + // Host element events + const elems = _hostElems(container); + elems.forEach((elem) => { + elem.addEventListener('mousedown', () => { + context.selectedWire = elem.getAttribute('data-wire'); + }); + + const gateElem = _equivGateElem(elem); + gateElem?.addEventListener('mousedown', (ev: MouseEvent) => { + ev.stopPropagation(); + context.selectedId = _equivDataId(elem); + dropzoneLayer.style.display = 'block'; + }); + }); - let parent = operations; - for (const segment of segments) { - parent = parent[segment].children || parent; - } - return parent; + // Dropzone element events + const dropzoneElems = dropzoneLayer.querySelectorAll('.dropzone'); + dropzoneElems.forEach((dropzoneElem) => { + dropzoneElem.addEventListener('mouseup', (ev: MouseEvent) => { + const targetId = dropzoneElem.getAttribute('data-dropzone-id'); + const targetWire = dropzoneElem.getAttribute('data-dropzone-wire'); + if ( + targetId == null || // + targetWire == null || + context.selectedId == null || + context.selectedWire == null + ) + return; + + const newSourceOperation = ev.ctrlKey + ? _copyX(context.selectedId, targetId, operations) + : _moveX(context.selectedId, targetId, operations); + + if (newSourceOperation != null) { + _moveY(context.selectedWire, targetWire, newSourceOperation, context.wireData); + } + + renderFn(); + }); + }); }; -const getGate = (dataId: string, operations: Operation[]): Operation => { - const parent = getParent(dataId, operations); - const index = splitDataId(dataId).pop(); +const _equivOperationParent = (dataId: string | null, operations: Operation[]): Operation[] | null => { + if (!dataId) return null; - if (index == null) { - throw new Error('Gate not found'); - } + const indexes = _indexes(dataId); + indexes.pop(); - return parent[index]; + let operationParent = operations; + for (const index of indexes) { + operationParent = operationParent[index].children || operationParent; + } + return operationParent; }; -// Utilities -const getRenderFn = (container: HTMLElement, sqore: Sqore, onCircuitChange?: () => void): (() => void) => { - return () => { - sqore.draw(container, 0, true, onCircuitChange); - if (onCircuitChange) onCircuitChange(); - }; -}; +const _equivOperation = (dataId: string | null, operations: Operation[]): Operation | null => { + if (!dataId) return null; -const getDataId = (element: Element): string => { - return element.getAttribute('data-id') || ''; -}; + const index = _lastIndex(dataId); + const operationParent = _equivOperationParent(dataId, operations); -const splitDataId = (dataId: string): number[] => { - return dataId === '' ? [] : dataId.split('-').map(Number); + if ( + operationParent == null || // + index == null + ) + return null; + + return operationParent[index]; }; -const getWireElemsY = (container: HTMLElement): Wires => { - const wireElems = getWireElems(container); - return wireElems.reduce((previous, current) => { - const y = getWireElemY(current); - const text = getWireElemText(current); - return { ...previous, [`${y}`]: text }; - }, {}); +const _moveX = (sourceId: string, targetId: string, operations: Operation[]) => { + if (sourceId === targetId) return _equivOperation(sourceId, operations); + const sourceOperation = _equivOperation(sourceId, operations); + const sourceOperationParent = _equivOperationParent(sourceId, operations); + const targetOperationParent = _equivOperationParent(targetId, operations); + const targetLastIndex = _lastIndex(targetId); + + if ( + targetOperationParent == null || // + targetLastIndex == null || + sourceOperation == null || + sourceOperationParent == null + ) + return; + + // Insert sourceOperation to target last index + const newSourceOperation: Operation = { ...sourceOperation }; + targetOperationParent.splice(targetLastIndex, 0, newSourceOperation); + + // Delete sourceOperation + sourceOperation.gate = 'removed'; + const indexToRemove = sourceOperationParent.findIndex((operation) => operation.gate === 'removed'); + sourceOperationParent.splice(indexToRemove, 1); + + return newSourceOperation; }; -const getWireElemY = (wireElem: SVGGElement): number => { - const lineElem = wireElem.querySelector('line'); - if (lineElem == null || lineElem.getAttribute('y1') == null) throw Error('y not found'); - return Number(lineElem.getAttribute('y1')); +const _copyX = (sourceId: string, targetId: string, operations: Operation[]) => { + if (sourceId === targetId) return _equivOperation(sourceId, operations); + const sourceOperation = _equivOperation(sourceId, operations); + const sourceOperationParent = _equivOperationParent(sourceId, operations); + const targetOperationParent = _equivOperationParent(targetId, operations); + const targetLastIndex = _lastIndex(targetId); + + if ( + targetOperationParent == null || // + targetLastIndex == null || + sourceOperation == null || + sourceOperationParent == null + ) + return; + + // Insert sourceOperation to target last index + const newSourceOperation: Operation = JSON.parse(JSON.stringify(sourceOperation)); + targetOperationParent.splice(targetLastIndex, 0, newSourceOperation); + + return newSourceOperation; }; -const getWireElemText = (wireElem: SVGGElement): string => { - const textElem = wireElem.querySelector('text'); - if (textElem == null || textElem.textContent == null || textElem.textContent === '') - throw new Error('Text not found'); - return textElem.textContent; +const _moveY = (sourceWire: string, targetWire: string, operation: Operation, wireData: number[]) => { + const offset = parseInt(targetWire) - parseInt(sourceWire); + _offsetRecursively(offset, operation, wireData); }; -const getClosestWireY = (offsetY: number, wires: { [y: number]: string }): number | null => { - for (const wire of Object.entries(wires)) { - const y = wire[0]; - const distance = Math.abs(offsetY - Number(y)); - // 15 is an arbitrary number to determine the closeness of distance - if (distance <= 15) { - return Number(y); - } +const _offsetRecursively = (offsetY: number, operation: Operation, wireData: number[]) => { + const wireDataSize = wireData.length; + + // Offset all targets by offsetY value + if (operation.targets != null) { + operation.targets.forEach((target) => { + target.qId = _circularMod(target.qId, offsetY, wireDataSize); + if (target.cId) target.cId = _circularMod(target.cId, offsetY, wireDataSize); + }); } - return null; -}; -const getDropzonePosition = (element: SVGElement): string => { - const position = element.getAttribute('data-dropzone-position'); - if (position == null) throw new Error('Position not found'); - return position; -}; + // Offset all controls by offsetY value + if (operation.controls != null) { + operation.controls.forEach((control) => { + control.qId = _circularMod(control.qId, offsetY, wireDataSize); + if (control.cId) control.cId = _circularMod(control.qId, offsetY, wireDataSize); + }); + } -const insertBefore = (parent: Operation[], index: number, newGate: Operation): void => { - parent.splice(index, 0, newGate); + // Offset recursively through all children + if (operation.children != null) { + operation.children.forEach((child) => _offsetRecursively(offsetY, child, wireData)); + } }; -const insertAfter = (parent: Operation[], index: number, newGate: Operation): void => { - parent.splice(index + 1, 0, newGate); +/** + * This modulo function always returns positive value based on total. + * i.e: value=0, offset=-1, total=4 returns 3 instead of -1 + */ +const _circularMod = (value: number, offset: number, total: number) => { + return (((value + offset) % total) + total) % total; }; -const deleteAt = (parent: Operation[], index: number): void => { - parent.splice(index, 1); -}; +const _indexes = (dataId: string) => dataId.split('-').map((segment) => parseInt(segment)); -const cursorMove = (container: HTMLElement, value: boolean): void => { - value ? container.classList.add('moving') : container.classList.remove('moving'); +const _lastIndex = (dataId: string) => { + return _indexes(dataId).pop(); }; -const cursorCopy = (container: HTMLElement, value: boolean): void => { - value ? container.classList.add('copying') : container.classList.remove('copying'); +const _renderFn = ( + container: HTMLElement, + sqore: Sqore, + onCircuitChange?: (circuit: Circuit) => void, +): (() => void) => { + return () => { + sqore.draw(container, 0, true, onCircuitChange); + if (onCircuitChange) onCircuitChange(sqore.circuit); + }; }; -const exportedForTesting = { - // addEditable - addDropzones, - addDocumentEvents, - addDropzoneEvents, - addMouseEvents, - handleGateMouseDown, - // handleDropzoneMouseUp - getGateElems, - getWireElems, - createDropzone, - createLeftDropzone, - createRightDropzone, - getParent, - getGate, - getDataId, - getRenderFn, - splitDataId, - getWireElemsY, - getWireElemY, - getWireElemText, - getClosestWireY, - getDropzonePosition, - insertBefore, - insertAfter, - deleteAt, - cursorMove, - cursorCopy, -}; +const exportedForTesting = {}; export { addEditable, exportedForTesting }; diff --git a/src/formatters/gateFormatter.ts b/src/formatters/gateFormatter.ts index 16588ff3..607bdc16 100644 --- a/src/formatters/gateFormatter.ts +++ b/src/formatters/gateFormatter.ts @@ -302,7 +302,7 @@ const _cross = (x: number, y: number): SVGElement => { const radius = 8; const line1: SVGElement = line(x - radius, y - radius, x + radius, y + radius); const line2: SVGElement = line(x - radius, y + radius, x + radius, y - radius); - return group([line1, line2]); + return group([line1, line2], { class: 'cross' }); }; /** diff --git a/src/index.ts b/src/index.ts index 5e5d770f..56b35003 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,7 @@ export const draw = ( style: StyleConfig | string = {}, renderDepth = 0, isEditable?: boolean, - onCircuitChange?: () => void, + onCircuitChange?: (circuit: Circuit) => void, ): void => { const sqore = new Sqore(circuit, style); diff --git a/src/sqore.ts b/src/sqore.ts index 878eef24..17934f74 100644 --- a/src/sqore.ts +++ b/src/sqore.ts @@ -1,16 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { formatInputs } from './formatters/inputFormatter'; +import { Circuit, ConditionalRender, Operation } from './circuit'; +import { svgNS } from './constants'; +import { addEditable } from './editable'; import { formatGates } from './formatters/gateFormatter'; +import { formatInputs } from './formatters/inputFormatter'; import { formatRegisters } from './formatters/registerFormatter'; +import { GateType, Metadata } from './metadata'; import { processOperations } from './process'; -import { ConditionalRender, Circuit, Operation } from './circuit'; -import { Metadata, GateType } from './metadata'; -import { StyleConfig, style, STYLES } from './styles'; +import { style, StyleConfig, STYLES } from './styles'; import { createUUID } from './utils'; -import { svgNS } from './constants'; -import { addEditable } from './editable'; /** * Contains metadata for visualization. @@ -59,7 +59,12 @@ export class Sqore { * @param isEditable Optional value enabling/disabling editable feature * @param onCircuitChange Optional function to trigger when changing elements in circuit */ - draw(container: HTMLElement, renderDepth = 0, isEditable?: boolean, onCircuitChange?: () => void): void { + draw( + container: HTMLElement, + renderDepth = 0, + isEditable?: boolean, + onCircuitChange?: (circuit: Circuit) => void, + ): void { // Inject into container if (container == null) throw new Error(`Container not provided.`); @@ -115,7 +120,7 @@ export class Sqore { container: HTMLElement, circuit: Circuit, isEditable?: boolean, - onCircuitChange?: () => void, + onCircuitChange?: (circuit: Circuit) => void, ): void { // Create visualization components const composedSqore: ComposedSqore = this.compose(circuit); @@ -250,7 +255,7 @@ export class Sqore { container: HTMLElement, circuit: Circuit, isEditable?: boolean, - onCircuitChange?: () => void, + onCircuitChange?: (circuit: Circuit) => void, ): void { this.addClassicalControlHandlers(container); this.addZoomHandlers(container, circuit, isEditable, onCircuitChange); @@ -317,7 +322,7 @@ export class Sqore { container: HTMLElement, circuit: Circuit, isEditable?: boolean, - onCircuitChange?: () => void, + onCircuitChange?: (circuit: Circuit) => void, ): void { container.querySelectorAll('.gate .gate-control').forEach((ctrl) => { // Zoom in on clicked gate diff --git a/src/styles.ts b/src/styles.ts index 10327841..481aea8c 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -237,53 +237,19 @@ const _expandCollapse = ` }`; const _editable = ` - .dropzone { - fill: transparent; - stroke: transparent; - } - .dropzone:hover{ - fill: red; - opacity: 25%; - } - text { - user-select: none; - } - .copying { - cursor: copy; - } - .moving { - cursor: move; - } - .detail-panel { - display: flex; - align-content: center; - gap: 12px; - }.dropzone { - fill: transparent; - stroke: transparent; - } - .dropzone:hover{ - fill: red; - opacity: 25%; - } text { user-select: none; + pointer-events: none; } - .copying { - cursor: copy; - } - .moving { - cursor: move; - } - .detail-panel { - display: flex; - align-content: center; - gap: 12px; + .dropzone-layer { + display: none; } - g.gate:not([data-expanded=true]) rect { - cursor: grab; + .dropzone { + fill-opacity: 0%; + stroke-opacity: 0%; } - g.gate:not([data-expanded=true]) circle { - cursor: grab; + .dropzone:hover { + fill: #EC7063; + fill-opacity: 50%; } `; From 605c64c7706c3097a0e39ba420e3ffed9253dc8b Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 28 Jun 2022 13:11:56 -0400 Subject: [PATCH 030/108] Update snapshots --- __tests__/__snapshots__/editable.test.ts.snap | 1147 ----------------- .../__snapshots__/gateFormatter.test.ts.snap | 72 +- 2 files changed, 54 insertions(+), 1165 deletions(-) delete mode 100644 __tests__/__snapshots__/editable.test.ts.snap diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap deleted file mode 100644 index d8e2ae43..00000000 --- a/__tests__/__snapshots__/editable.test.ts.snap +++ /dev/null @@ -1,1147 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Testing addDocumentEvents verify document events 1`] = `
`; - -exports[`Testing addDocumentEvents verify document events 2`] = `
`; - -exports[`Testing addDropzoneEvents add 1 event 1`] = ` -
- - - -
-`; - -exports[`Testing addDropzoneEvents add 2 events 1`] = ` -
- - - - -
-`; - -exports[`Testing addDropzones verify dropzones 1`] = ` -
- - - - - |0⟩ - - - |0⟩ - - - - - - - q0 - - - - - - q1 - - - - - - - - - - - - - - - - H - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-`; - -exports[`Testing addMouseEvents verify mouse events 1`] = ` -
- - - - - |0⟩ - - - |0⟩ - - - - - - - q0 - - - - - - q1 - - - - - - - - - - - - - - - - H - - - - - - - - - - - - - -
-`; - -exports[`Testing createDropzone create dropzone on the left 1`] = ` - -`; - -exports[`Testing createDropzone create dropzone on the right 1`] = ` - -`; - -exports[`Testing createLeftDropzone create left dropzone 1`] = ` - -`; - -exports[`Testing createRightDropzone create dropzone right 1`] = ` - -`; - -exports[`Testing cursorCopy turn off copy cursor 1`] = ` -
-`; - -exports[`Testing cursorCopy turn off copy cursor 2`] = ` -
-`; - -exports[`Testing cursorCopy turn on and off copy cursor 1`] = ` -
-`; - -exports[`Testing cursorCopy turn on and off copy cursor 2`] = ` -
-`; - -exports[`Testing cursorCopy turn on copy cursor 1`] = `
`; - -exports[`Testing cursorCopy turn on copy cursor 2`] = ` -
-`; - -exports[`Testing cursorMove turn off move cursor 1`] = ` -
-`; - -exports[`Testing cursorMove turn off move cursor 2`] = ` -
-`; - -exports[`Testing cursorMove turn on and off move cursor 1`] = ` -
-`; - -exports[`Testing cursorMove turn on and off move cursor 2`] = ` -
-`; - -exports[`Testing cursorMove turn on move cursor 1`] = `
`; - -exports[`Testing cursorMove turn on move cursor 2`] = ` -
-`; - -exports[`Testing getGateElems get 2 gates 1`] = ` -Array [ - - - - - - H - - - - , - - - - - - - , -] -`; - -exports[`Testing getGateElems get 3 gates 1`] = ` -Array [ - - - - - - H - - - - , - - - - - - - - - , - - - - - - - , -] -`; - -exports[`Testing getWireElems get 2 wires 1`] = ` -Array [ - - - - q0 - - , - - - - q1 - - , -] -`; - -exports[`Testing handleGateMouseDown copying, ctrlKey is true 1`] = ` -
-`; - -exports[`Testing handleGateMouseDown moving, ctrlKey is false 1`] = ` -
-`; diff --git a/__tests__/__snapshots__/gateFormatter.test.ts.snap b/__tests__/__snapshots__/gateFormatter.test.ts.snap index e75b6f54..5596850a 100644 --- a/__tests__/__snapshots__/gateFormatter.test.ts.snap +++ b/__tests__/__snapshots__/gateFormatter.test.ts.snap @@ -1326,7 +1326,9 @@ exports[`Testing _controlledGate SWAP gate 1`] = ` cy="40" r="5" /> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + Date: Tue, 28 Jun 2022 13:12:17 -0400 Subject: [PATCH 031/108] Update tests --- __tests__/editable.test.ts | 1344 +----------------------------------- src/editable.ts | 9 +- 2 files changed, 44 insertions(+), 1309 deletions(-) diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index 50e7762d..209f21bd 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -1,1327 +1,59 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { Circuit, Operation } from '../src/circuit'; import { exportedForTesting } from '../src/editable'; -import { RegisterType } from '../src/register'; -import { draw, STYLES } from '../src/index'; -import { Sqore } from '../src/sqore'; -const { - // addEditable - addDropzones, - addDocumentEvents, - addDropzoneEvents, - addMouseEvents, - handleGateMouseDown, - // handleDropzoneMouseUp - getGateElems, - getWireElems, - createDropzone, - createLeftDropzone, - createRightDropzone, - getParent, - getGate, - getRenderFn, - getDataId, - splitDataId, - getWireElemsY, - getWireElemY, - getWireElemText, - getClosestWireY, - getDropzonePosition, - insertBefore, - insertAfter, - deleteAt, - cursorMove, - cursorCopy, -} = exportedForTesting; +const { _indexes, _lastIndex, _center } = exportedForTesting; -// Utlities -describe('Testing getDataId', () => { - const elem = document.createElement('div'); - test('with with no data-id', () => { - expect(getDataId(elem)).toBe(''); - }); - test('with level 0 data-id', () => { - elem.setAttribute('data-id', '0'); - expect(getDataId(elem)).toBe('0'); - }); - - test('with level 1 data-id', () => { - elem.setAttribute('data-id', '0-1'); - expect(getDataId(elem)).toBe('0-1'); - }); -}); - -describe('Testing splitDataId', () => { - test('with empty dataId', () => { - expect(splitDataId('')).toStrictEqual([]); - }); - test('with level 0 data-id', () => { - expect(splitDataId('1')).toStrictEqual([1]); - }); - - test('with level 1 data-id', () => { - expect(splitDataId('1-2')).toStrictEqual([1, 2]); - }); -}); - -describe('Testing cursorMove', () => { - const container = document.createElement('div'); - test('turn on move cursor', () => { - expect(container).toMatchSnapshot(); - cursorMove(container, true); - expect(container).toMatchSnapshot(); - }); - test('turn off move cursor', () => { - expect(container).toMatchSnapshot(); - cursorMove(container, false); - expect(container).toMatchSnapshot(); - }); - test('turn on and off move cursor', () => { - expect(container).toMatchSnapshot(); - cursorMove(container, true); - cursorMove(container, false); - expect(container).toMatchSnapshot(); - }); -}); - -describe('Testing cursorCopy', () => { - const container = document.createElement('div'); - test('turn on copy cursor', () => { - expect(container).toMatchSnapshot(); - cursorCopy(container, true); - expect(container).toMatchSnapshot(); - }); - test('turn off copy cursor', () => { - expect(container).toMatchSnapshot(); - cursorCopy(container, false); - expect(container).toMatchSnapshot(); - }); - test('turn on and off copy cursor', () => { - expect(container).toMatchSnapshot(); - cursorCopy(container, true); - cursorCopy(container, false); - expect(container).toMatchSnapshot(); - }); -}); - -describe('Testing deleteAt', () => { - const operations: Operation[] = []; - beforeEach(() => { - Object.assign(operations, [ - { - gate: 'X', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - { - gate: 'Y', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - { - gate: 'Z', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - ]); - }); - test('delete X at index 0', () => { - deleteAt(operations, 0); - expect(operations).toStrictEqual([ - { - gate: 'Y', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - { - gate: 'Z', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - ]); - }); - test('delete Y at index 1', () => { - deleteAt(operations, 1); - expect(operations).toStrictEqual([ - { - gate: 'X', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - { - gate: 'Z', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - ]); - }); - test('delete Z and X at index 2, 0', () => { - deleteAt(operations, 2); - deleteAt(operations, 0); - expect(operations).toStrictEqual([ - { - gate: 'Y', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - ]); - }); -}); - -describe('Testing insertBefore', () => { - test('insert before X', () => { - const operations = [ - { - gate: 'X', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - { - gate: 'Z', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - ]; - const newGate = { - gate: 'Y', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }; - insertBefore(operations, 0, newGate); - expect(operations).toStrictEqual([ - { - gate: 'Y', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - { - gate: 'X', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - { - gate: 'Z', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - ]); - }); -}); - -describe('Testing insertAfter', () => { - test('insert after X', () => { - const operations = [ - { - gate: 'X', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - { - gate: 'Z', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - ]; - const newGate = { - gate: 'Y', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }; - insertAfter(operations, 0, newGate); - expect(operations).toStrictEqual([ - { - gate: 'X', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - { - gate: 'Y', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - { - gate: 'Z', - isMeasurement: false, - isConditional: false, - isControlled: false, - isAdjoint: false, - controls: [], - targets: [{ type: RegisterType.Qubit, qId: 0 }], - }, - ]); - }); -}); - -describe('Testing getDropzonePosition', () => { - let svgElem: SVGElement; - let dropzoneElem: SVGElement; - beforeEach(() => { - svgElem = document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGElement; - dropzoneElem = document.createElementNS('http://www.w3.org/2000/svg', 'rect') as SVGElement; - svgElem.append(dropzoneElem); - }); - test('get position of non-dropzone', () => { - expect(() => getDropzonePosition(dropzoneElem)).toThrowError('Position not found'); - }); - test('get position of dropzone on the left', () => { - dropzoneElem.setAttribute('data-dropzone-position', 'left'); - expect(getDropzonePosition(dropzoneElem)).toBe('left'); - }); - test('get position of dropzone on the right', () => { - dropzoneElem.setAttribute('data-dropzone-position', 'right'); - expect(getDropzonePosition(dropzoneElem)).toBe('right'); - }); -}); - -describe('Testing getWireElementText', () => { - let svgElem: SVGElement; - let groupElem: SVGGElement; - let textElem: SVGGElement; - beforeEach(() => { - svgElem = document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGElement; - groupElem = document.createElementNS('http://www.w3.org/2000/svg', 'g') as SVGGElement; - textElem = document.createElementNS('http://www.w3.org/2000/svg', 'text') as SVGTextElement; - groupElem.append(textElem); - svgElem.append(groupElem); - }); - test('text element not exists', () => { - textElem.remove(); - expect(() => getWireElemText(groupElem)).toThrowError('Text not found'); - }); - test('get text element without textContent', () => { - expect(() => getWireElemText(groupElem)).toThrowError('Text not found'); - }); - test('get text element empty textContent', () => { - expect(() => getWireElemText(groupElem)).toThrowError('Text not found'); - }); - test('should return q0', () => { - textElem.textContent = 'q0'; - expect(getWireElemText(groupElem)).toEqual('q0'); - }); - test('should return q1', () => { - textElem.textContent = 'q1'; - expect(getWireElemText(groupElem)).toEqual('q1'); - }); -}); - -describe('Testing getWireElemY', () => { - let svgElem: SVGElement; - let groupElem: SVGGElement; - let lineElem: SVGGElement; - beforeEach(() => { - svgElem = document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGElement; - groupElem = document.createElementNS('http://www.w3.org/2000/svg', 'g') as SVGGElement; - lineElem = document.createElementNS('http://www.w3.org/2000/svg', 'line') as SVGLineElement; - groupElem.append(lineElem); - svgElem.append(groupElem); - }); - test('line element not exists', () => { - lineElem.remove(); - expect(() => getWireElemY(groupElem)).toThrowError('y not found'); - }); - test('get y element without y value', () => { - expect(() => getWireElemY(groupElem)).toThrowError('y not found'); - }); - test('get text element empty textContent', () => { - expect(() => getWireElemY(groupElem)).toThrowError('y not found'); - }); - test('should return 40', () => { - lineElem.setAttribute('y1', '40'); - expect(getWireElemY(groupElem)).toEqual(40); - }); - test('should return 99', () => { - lineElem.setAttribute('y1', '99'); - expect(getWireElemY(groupElem)).toEqual(99); - }); -}); - -describe('Testing getParent', () => { - test('with level 0 gate', () => { - const operations = [ - { - gate: 'H', - targets: [{ qId: 0 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - }, - { - gate: 'Measure', - isMeasurement: true, - controls: [{ qId: 1 }], - targets: [{ type: 1, qId: 1, cId: 0 }], - }, - ]; - expect(getParent('0', operations)).toStrictEqual([ - { - gate: 'H', - targets: [{ qId: 0 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - }, - { - gate: 'Measure', - isMeasurement: true, - controls: [{ qId: 1 }], - targets: [{ type: 1, qId: 1, cId: 0 }], - }, - ]); - }); - test('with level 1 gate', () => { - const operations = [ - { - gate: 'Foo', - conditionalRender: 3, - targets: [{ qId: 0 }, { qId: 1 }], - children: [ - { - gate: 'H', - targets: [{ qId: 1 }], - }, - { - gate: 'RX', - displayArgs: '(0.25)', - isControlled: true, - controls: [{ qId: 1 }], - targets: [{ qId: 0 }], - }, - ], - }, - { - gate: 'X', - targets: [{ qId: 3 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 1 }], - targets: [{ qId: 2 }, { qId: 3 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 2 }, { qId: 3 }], - targets: [{ qId: 1 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 1 }, { qId: 3 }], - targets: [{ qId: 2 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 2 }], - targets: [{ qId: 1 }, { qId: 3 }], - }, - { - gate: 'measure', - isMeasurement: true, - controls: [{ qId: 0 }], - targets: [{ type: 1, qId: 0, cId: 0 }], - }, - { - gate: 'ApplyIfElseR', - isConditional: true, - controls: [{ type: 1, qId: 0, cId: 0 }], - targets: [], - children: [ - { - gate: 'H', - targets: [{ qId: 1 }], - conditionalRender: 1, - }, - { - gate: 'X', - targets: [{ qId: 1 }], - conditionalRender: 1, - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - conditionalRender: 2, - }, - { - gate: 'Foo', - targets: [{ qId: 3 }], - conditionalRender: 2, - }, - ], - }, - { - gate: 'SWAP', - targets: [{ qId: 0 }, { qId: 2 }], - children: [ - { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, - { gate: 'X', isControlled: true, controls: [{ qId: 2 }], targets: [{ qId: 0 }] }, - { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, - ], - }, - { - gate: 'ZZ', - targets: [{ qId: 1 }, { qId: 3 }], - }, - { - gate: 'ZZ', - targets: [{ qId: 0 }, { qId: 1 }], - }, - { - gate: 'XX', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }, { qId: 3 }], - }, - { - gate: 'XX', - isControlled: true, - controls: [{ qId: 2 }], - targets: [{ qId: 1 }, { qId: 3 }], - }, - { - gate: 'XX', - isControlled: true, - controls: [{ qId: 0 }, { qId: 2 }], - targets: [{ qId: 1 }, { qId: 3 }], - }, - ]; - expect(getParent('0-1', operations)).toStrictEqual([ - { - gate: 'H', - targets: [{ qId: 1 }], - }, - { - gate: 'RX', - displayArgs: '(0.25)', - isControlled: true, - controls: [{ qId: 1 }], - targets: [{ qId: 0 }], - }, - ]); - }); -}); - -describe('Testing getGate', () => { - test('should return H gate', () => { - const operations = [ - { - gate: 'H', - targets: [{ qId: 0 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - }, - { - gate: 'Measure', - isMeasurement: true, - controls: [{ qId: 1 }], - targets: [{ type: 1, qId: 1, cId: 0 }], - }, - ]; - expect(getGate('0', operations)).toStrictEqual({ - gate: 'H', - targets: [{ qId: 0 }], - }); - }); - test('should return X gate', () => { - const operations = [ - { - gate: 'H', - targets: [{ qId: 0 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - }, - { - gate: 'Measure', - isMeasurement: true, - controls: [{ qId: 1 }], - targets: [{ type: 1, qId: 1, cId: 0 }], - }, - ]; - expect(getGate('1', operations)).toStrictEqual({ - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - }); - }); - test('should return RX', () => { - const operations = [ - { - gate: 'Foo', - conditionalRender: 3, - targets: [{ qId: 0 }, { qId: 1 }], - children: [ - { - gate: 'H', - targets: [{ qId: 1 }], - }, - { - gate: 'RX', - displayArgs: '(0.25)', - isControlled: true, - controls: [{ qId: 1 }], - targets: [{ qId: 0 }], - }, - ], - }, - { - gate: 'X', - targets: [{ qId: 3 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 1 }], - targets: [{ qId: 2 }, { qId: 3 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 2 }, { qId: 3 }], - targets: [{ qId: 1 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 1 }, { qId: 3 }], - targets: [{ qId: 2 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 2 }], - targets: [{ qId: 1 }, { qId: 3 }], - }, - { - gate: 'measure', - isMeasurement: true, - controls: [{ qId: 0 }], - targets: [{ type: 1, qId: 0, cId: 0 }], - }, - { - gate: 'ApplyIfElseR', - isConditional: true, - controls: [{ type: 1, qId: 0, cId: 0 }], - targets: [], - children: [ - { - gate: 'H', - targets: [{ qId: 1 }], - conditionalRender: 1, - }, - { - gate: 'X', - targets: [{ qId: 1 }], - conditionalRender: 1, - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - conditionalRender: 2, - }, - { - gate: 'Foo', - targets: [{ qId: 3 }], - conditionalRender: 2, - }, - ], - }, - { - gate: 'SWAP', - targets: [{ qId: 0 }, { qId: 2 }], - children: [ - { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, - { gate: 'X', isControlled: true, controls: [{ qId: 2 }], targets: [{ qId: 0 }] }, - { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, - ], - }, - { - gate: 'ZZ', - targets: [{ qId: 1 }, { qId: 3 }], - }, - { - gate: 'ZZ', - targets: [{ qId: 0 }, { qId: 1 }], - }, - { - gate: 'XX', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }, { qId: 3 }], - }, - { - gate: 'XX', - isControlled: true, - controls: [{ qId: 2 }], - targets: [{ qId: 1 }, { qId: 3 }], - }, - { - gate: 'XX', - isControlled: true, - controls: [{ qId: 0 }, { qId: 2 }], - targets: [{ qId: 1 }, { qId: 3 }], - }, - ]; - expect(getGate('0-1', operations)).toStrictEqual({ - gate: 'RX', - displayArgs: '(0.25)', - isControlled: true, - controls: [{ qId: 1 }], - targets: [{ qId: 0 }], - }); - }); -}); - -describe('Testing addDocumentEvents', () => { - test('verify document events', () => { - const container = document.createElement('div'); - expect(container).toMatchSnapshot(); - addDocumentEvents(container); - expect(container).toMatchSnapshot(); - }); -}); - -describe('Testing handleGateMouseDown', () => { - test('copying, ctrlKey is true', () => { - const container = document.createElement('div'); - const ev = new MouseEvent('mousedown', { ctrlKey: true }); - handleGateMouseDown(ev, container); - expect(container).toMatchSnapshot(); - }); - test('moving, ctrlKey is false', () => { - const container = document.createElement('div'); - const ev = new MouseEvent('mousedown', { ctrlKey: false }); - handleGateMouseDown(ev, container); - expect(container).toMatchSnapshot(); - }); -}); - -describe('Testing getGateElems', () => { - test('get 2 gates', () => { - const container = document.createElement('div'); - const circuit = { - qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], - operations: [ - { - gate: 'H', - targets: [{ qId: 0 }], - }, - { - gate: 'Measure', - isMeasurement: true, - controls: [{ qId: 1 }], - targets: [{ type: 1, qId: 1, cId: 0 }], - }, - ], - }; - draw(circuit, container, STYLES['default']); - const gateElems = getGateElems(container); - expect(gateElems).toHaveLength(2); - expect(gateElems).toMatchSnapshot(); - }); - test('get 3 gates', () => { - const container = document.createElement('div'); - const circuit = { - qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], - operations: [ - { - gate: 'H', - targets: [{ qId: 0 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - }, - { - gate: 'Measure', - isMeasurement: true, - controls: [{ qId: 1 }], - targets: [{ type: 1, qId: 1, cId: 0 }], - }, - ], - }; - draw(circuit, container, STYLES['default']); - const gateElems = getGateElems(container); - expect(gateElems).toHaveLength(3); - expect(gateElems).toMatchSnapshot(); - }); -}); - -describe('Testing getWireElems', () => { - test('get 2 wires', () => { - const container = document.createElement('div'); - const circuit = { - qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], - operations: [ - { - gate: 'H', - targets: [{ qId: 0 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - }, - { - gate: 'Measure', - isMeasurement: true, - controls: [{ qId: 1 }], - targets: [{ type: 1, qId: 1, cId: 0 }], - }, - ], - }; - draw(circuit, container, STYLES['default']); - const wireElems = getWireElems(container); - expect(wireElems).toHaveLength(2); - expect(wireElems).toMatchSnapshot(); - }); -}); - -describe('Testing createDropzone', () => { - test('create dropzone on the left', () => { - expect(createDropzone(0, 0, 20, 20, '0', 'left')).toMatchSnapshot(); - }); - test('create dropzone on the right', () => { - expect(createDropzone(0, 0, 20, 20, '0', 'right')).toMatchSnapshot(); - }); -}); - -describe('Testing createLeftDropzone', () => { - test('create left dropzone', () => { - expect(createLeftDropzone(0, 0, 20, '0')).toMatchSnapshot(); - }); -}); - -describe('Testing createRightDropzone', () => { - test('create dropzone right', () => { - expect(createRightDropzone(0, 0, 20, 20, '0')).toMatchSnapshot(); - }); -}); - -describe('Testing getClosestWireY', () => { - test('should return 40', () => { - const container = document.createElement('div'); - const circuit = { - qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], - operations: [ - { - gate: 'H', - targets: [{ qId: 0 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - }, - { - gate: 'Measure', - isMeasurement: true, - controls: [{ qId: 1 }], - targets: [{ type: 1, qId: 1, cId: 0 }], - }, - ], - }; - const wires = { '40': 'q0', '100': 'q1' }; - draw(circuit, container, STYLES['default']); - expect(getClosestWireY(50, wires)).toEqual(40); - }); - test('should return 100', () => { - const container = document.createElement('div'); - const circuit = { - qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], - operations: [ - { - gate: 'H', - targets: [{ qId: 0 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - }, - { - gate: 'Measure', - isMeasurement: true, - controls: [{ qId: 1 }], - targets: [{ type: 1, qId: 1, cId: 0 }], - }, - ], - }; - const wires = { '40': 'q0', '100': 'q1' }; - draw(circuit, container, STYLES['default']); - expect(getClosestWireY(85, wires)).toEqual(100); - }); - test('should return null', () => { - const container = document.createElement('div'); - const circuit = { - qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], - operations: [ - { - gate: 'H', - targets: [{ qId: 0 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - }, - { - gate: 'Measure', - isMeasurement: true, - controls: [{ qId: 1 }], - targets: [{ type: 1, qId: 1, cId: 0 }], - }, - ], - }; - const wires = { '40': 'q0', '100': 'q1' }; - draw(circuit, container, STYLES['default']); - expect(getClosestWireY(120, wires)).toEqual(null); - }); -}); - -describe('test getWireElemsY', () => { - test('get 2 wires', () => { - const container = document.createElement('div'); - const circuit = { - qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], - operations: [ - { - gate: 'H', - targets: [{ qId: 0 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - }, - { - gate: 'Measure', - isMeasurement: true, - controls: [{ qId: 1 }], - targets: [{ type: 1, qId: 1, cId: 0 }], - }, - ], - }; - const expected = { '40': 'q0', '100': 'q1' }; - draw(circuit, container, STYLES['default']); - expect(getWireElemsY(container)).toStrictEqual(expected); - }); - test('get 4 wires', () => { - const container = document.createElement('div'); - const circuit = { - qubits: [{ id: 0, numChildren: 1 }, { id: 1 }, { id: 2 }, { id: 3 }], - operations: [ - { - gate: 'Foo', - conditionalRender: 3, - targets: [{ qId: 0 }, { qId: 1 }], - children: [ - { - gate: 'H', - targets: [{ qId: 1 }], - }, - { - gate: 'RX', - displayArgs: '(0.25)', - isControlled: true, - controls: [{ qId: 1 }], - targets: [{ qId: 0 }], - }, - ], - }, - { - gate: 'X', - targets: [{ qId: 3 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 1 }], - targets: [{ qId: 2 }, { qId: 3 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 2 }, { qId: 3 }], - targets: [{ qId: 1 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 1 }, { qId: 3 }], - targets: [{ qId: 2 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 2 }], - targets: [{ qId: 1 }, { qId: 3 }], - }, - { - gate: 'measure', - isMeasurement: true, - controls: [{ qId: 0 }], - targets: [{ type: 1, qId: 0, cId: 0 }], - }, - { - gate: 'ApplyIfElseR', - isConditional: true, - controls: [{ type: 1, qId: 0, cId: 0 }], - targets: [], - children: [ - { - gate: 'H', - targets: [{ qId: 1 }], - conditionalRender: 1, - }, - { - gate: 'X', - targets: [{ qId: 1 }], - conditionalRender: 1, - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - conditionalRender: 2, - }, - { - gate: 'Foo', - targets: [{ qId: 3 }], - conditionalRender: 2, - }, - ], - }, - { - gate: 'SWAP', - targets: [{ qId: 0 }, { qId: 2 }], - children: [ - { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, - { gate: 'X', isControlled: true, controls: [{ qId: 2 }], targets: [{ qId: 0 }] }, - { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, - ], - }, - { - gate: 'ZZ', - targets: [{ qId: 1 }, { qId: 3 }], - }, - { - gate: 'ZZ', - targets: [{ qId: 0 }, { qId: 1 }], - }, - { - gate: 'XX', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }, { qId: 3 }], - }, - { - gate: 'XX', - isControlled: true, - controls: [{ qId: 2 }], - targets: [{ qId: 1 }, { qId: 3 }], - }, - { - gate: 'XX', - isControlled: true, - controls: [{ qId: 0 }, { qId: 2 }], - targets: [{ qId: 1 }, { qId: 3 }], - }, - ], - }; - const expected = { '40': 'q0', '120': 'q1', '180': 'q2', '240': 'q3' }; - draw(circuit, container, STYLES['default']); - expect(getWireElemsY(container)).toStrictEqual(expected); - }); -}); - -describe('Testing addDropzoneEvents', () => { - interface Context { - container: HTMLElement; - operations: Operation[]; - wires: Wires; - renderFn: () => void; - } - - interface Wires { - [y: string]: string; - } - - test('add 1 event', () => { - const container = document.createElement('div'); - const svgElem = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - const dropzoneElem = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - svgElem.append(dropzoneElem); - container.append(svgElem); - - const context: Context = { - container: container, - operations: [], - wires: {}, - renderFn: () => { - return; - }, - }; - addDropzoneEvents(context); - expect(container).toMatchSnapshot(); - }); - test('add 2 events', () => { - const container = document.createElement('div'); - const svgElem = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - const dropzoneElem = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - const dropzoneElem1 = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - svgElem.append(dropzoneElem); - svgElem.append(dropzoneElem1); - container.append(svgElem); - interface Context { - container: HTMLElement; - operations: Operation[]; - wires: Wires; - renderFn: () => void; - } - - const context: Context = { - container: container, - operations: [], - wires: {}, - renderFn: () => { - return; - }, - }; - addDropzoneEvents(context); - expect(container).toMatchSnapshot(); - }); -}); - -describe('Testing addMouseEvents', () => { - interface Context { - container: HTMLElement; - operations: Operation[]; - wires: Wires; - renderFn: () => void; - } - interface Wires { - [y: string]: string; - } - test('verify mouse events', () => { - const container = document.createElement('div'); - const circuit = { - qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], - operations: [ - { - gate: 'H', - targets: [{ qId: 0 }], - }, - { - gate: 'Measure', - isMeasurement: true, - controls: [{ qId: 1 }], - targets: [{ type: 1, qId: 1, cId: 0 }], - }, - ], - }; - draw(circuit, container, STYLES['default']); - const context: Context = { - container: container, - operations: [], - wires: {}, - renderFn: () => { - return; - }, - }; - const svgElem = container.querySelector('svg'); - if (svgElem != null) svgElem.removeAttribute('id'); - addMouseEvents(context); - expect(container).toMatchSnapshot(); - }); -}); - -describe('Testing getRenderFn', () => { - test('check console.log displaying "onCircuitChange is triggered"', () => { +describe('Test _center', () => { + test('should return {25,50}', () => { Object.defineProperty(window.SVGElement.prototype, 'getBBox', { writable: true, value: () => ({ x: 0, y: 0, - width: 0, - height: 0, + width: 50, + height: 100, }), }); - const container = document.createElement('div'); - const circuit: Circuit = { - qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], - operations: [ - { - gate: 'H', - targets: [{ qId: 0 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - }, - { - gate: 'Measure', - isMeasurement: true, - controls: [{ qId: 1 }], - targets: [{ type: 1, qId: 1, cId: 0 }], - }, - ], - }; - const sqore = new Sqore(circuit, STYLES['default']); - const onCircuitChange = () => console.log('onCircuitChange is triggered'); - const renderFn = getRenderFn(container, sqore, onCircuitChange); - - jest.spyOn(console, 'log'); - renderFn(); - expect(console.log).toHaveBeenCalledWith('onCircuitChange is triggered'); + const elem = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + expect(_center(elem)).toStrictEqual({ cX: 25, cY: 50 }); }); -}); - -describe('Testing addDropzones', () => { - test('verify dropzones', () => { + test('should return {105,210}', () => { Object.defineProperty(window.SVGElement.prototype, 'getBBox', { writable: true, value: () => ({ - x: 0, - y: 0, - width: 0, - height: 0, + x: 100, + y: 200, + width: 10, + height: 20, }), }); - const container = document.createElement('div'); - const circuit: Circuit = { - qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], - operations: [ - { - gate: 'H', - targets: [{ qId: 0 }], - }, - { - gate: 'X', - isControlled: true, - controls: [{ qId: 0 }], - targets: [{ qId: 1 }], - }, - { - gate: 'Measure', - isMeasurement: true, - controls: [{ qId: 1 }], - targets: [{ type: 1, qId: 1, cId: 0 }], - }, - ], - }; - draw(circuit, container, STYLES['default'], 0, true); - const svgElem = container.querySelector('svg'); - if (svgElem != null) svgElem.removeAttribute('id'); - addDropzones(container); - expect(container).toMatchSnapshot(); + const elem = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + expect(_center(elem)).toStrictEqual({ cX: 105, cY: 210 }); + }); +}); + +describe('Test _lastIndex', () => { + test('"" should return undefined', () => { + expect(_lastIndex('')).toBeUndefined(); + }); + test('"0-0-1" should return 1', () => { + expect(_lastIndex('0-0-1')).toEqual(1); + }); + test('"1-0-5" should return [1,0,5]', () => { + expect(_lastIndex('1-0-5')).toEqual(5); + }); +}); + +describe('Test _indexes', () => { + test('"" should return []', () => { + expect(_indexes('')).toStrictEqual([]); + }); + test('"0-0-1" should return [0,0,1]', () => { + expect(_indexes('0-0-1')).toStrictEqual([0, 0, 1]); + }); + test('"1-0-1" should return [1,0,1]', () => { + expect(_indexes('1-0-1')).toStrictEqual([1, 0, 1]); }); }); diff --git a/src/editable.ts b/src/editable.ts index befbf36f..9ef55d13 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -370,9 +370,12 @@ const _circularMod = (value: number, offset: number, total: number) => { return (((value + offset) % total) + total) % total; }; -const _indexes = (dataId: string) => dataId.split('-').map((segment) => parseInt(segment)); +const _indexes = (dataId: string): number[] => + dataId !== '' // + ? dataId.split('-').map((segment) => parseInt(segment)) + : []; -const _lastIndex = (dataId: string) => { +const _lastIndex = (dataId: string): number | undefined => { return _indexes(dataId).pop(); }; @@ -387,6 +390,6 @@ const _renderFn = ( }; }; -const exportedForTesting = {}; +const exportedForTesting = { _center, _indexes, _lastIndex }; export { addEditable, exportedForTesting }; From 4b22f075373e9096f70d1346049f4c8e90a49610 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 28 Jun 2022 13:43:38 -0400 Subject: [PATCH 032/108] Add tests for _center, _wireData --- __tests__/editable.test.ts | 58 +++++++++++++++++++++++++++++++++++++- src/editable.ts | 8 +++--- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index 209f21bd..66874c99 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -2,8 +2,9 @@ // Licensed under the MIT license. import { exportedForTesting } from '../src/editable'; +import { draw, STYLES } from '../src/index'; -const { _indexes, _lastIndex, _center } = exportedForTesting; +const { _center, _wireData, _indexes, _lastIndex } = exportedForTesting; describe('Test _center', () => { test('should return {25,50}', () => { @@ -34,6 +35,61 @@ describe('Test _center', () => { }); }); +describe('Test _wireData', () => { + test('2 wires should return [40,100]', () => { + const container = document.createElement('div'); + const circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + draw(circuit, container, STYLES['default']); + expect(_wireData(container)).toStrictEqual([40, 100]); + }); + test('3 wires should return [40,100, 180]', () => { + const container = document.createElement('div'); + const circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }, { id: 2 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 2 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + draw(circuit, container, STYLES['default']); + expect(_wireData(container)).toStrictEqual([40, 100, 180]); + }); +}); + describe('Test _lastIndex', () => { test('"" should return undefined', () => { expect(_lastIndex('')).toBeUndefined(); diff --git a/src/editable.ts b/src/editable.ts index 9ef55d13..679de361 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -87,7 +87,7 @@ const _wirePrefixes = (wireData: number[]) => wireData.map((wireY, index) => ({ /** * Find center point of element */ -const _center = (elem: SVGGraphicsElement) => { +const _center = (elem: SVGGraphicsElement): { cX: number; cY: number } => { const { x, y, width, height } = elem.getBBox(); return { cX: x + width / 2, cY: y + height / 2 }; }; @@ -161,7 +161,7 @@ const _dropzoneLayer = (context: Context) => { return dropzoneLayer; }; -const _wireData = (container: HTMLElement) => { +const _wireData = (container: HTMLElement): number[] => { // elems include qubit wires and lines of measure gates const elems = container.querySelectorAll('svg > g:nth-child(3) > g'); // filter out elements having more than 2 elements because @@ -170,7 +170,7 @@ const _wireData = (container: HTMLElement) => { const wireElems = Array.from(elems).filter((elem) => elem.childElementCount < 3); const wireData = wireElems.map((wireElem) => { const lineElem = wireElem.children[0] as SVGLineElement; - return lineElem.y1.baseVal.value; + return Number(lineElem.getAttribute('y1')); }); return wireData; }; @@ -390,6 +390,6 @@ const _renderFn = ( }; }; -const exportedForTesting = { _center, _indexes, _lastIndex }; +const exportedForTesting = { _center, _wireData, _indexes, _lastIndex }; export { addEditable, exportedForTesting }; From d975f8863c433c5dedd7ec4dfc9a8787277ca3fc Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 28 Jun 2022 15:30:25 -0400 Subject: [PATCH 033/108] Add tests for _equivOperation, _equivOperationParent --- __tests__/__snapshots__/editable.test.ts.snap | 100 ++++++++++ __tests__/editable.test.ts | 184 +++++++++++++++++- src/editable.ts | 9 +- 3 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 __tests__/__snapshots__/editable.test.ts.snap diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap new file mode 100644 index 00000000..6dd2e3a4 --- /dev/null +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test _equivOperation should return H gate 1`] = ` +Object { + "gate": "H", + "targets": Array [ + Object { + "qId": 0, + }, + ], +} +`; + +exports[`Test _equivOperation should return X gate 1`] = ` +Object { + "controls": Array [ + Object { + "qId": 0, + }, + ], + "gate": "X", + "isControlled": true, + "targets": Array [ + Object { + "qId": 1, + }, + ], +} +`; + +exports[`Test _equivOperationParent should return Foo 1`] = ` +Array [ + Object { + "gate": "H", + "targets": Array [ + Object { + "qId": 1, + }, + ], + }, + Object { + "controls": Array [ + Object { + "qId": 1, + }, + ], + "displayArgs": "(0.25)", + "gate": "RX", + "isControlled": true, + "targets": Array [ + Object { + "qId": 0, + }, + ], + }, +] +`; + +exports[`Test _equivOperationParent should return all operations 1`] = ` +Array [ + Object { + "gate": "H", + "targets": Array [ + Object { + "qId": 0, + }, + ], + }, + Object { + "controls": Array [ + Object { + "qId": 0, + }, + ], + "gate": "X", + "isControlled": true, + "targets": Array [ + Object { + "qId": 1, + }, + ], + }, + Object { + "controls": Array [ + Object { + "qId": 1, + }, + ], + "gate": "Measure", + "isMeasurement": true, + "targets": Array [ + Object { + "cId": 0, + "qId": 1, + "type": 1, + }, + ], + }, +] +`; diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index 66874c99..102fbc10 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -4,7 +4,7 @@ import { exportedForTesting } from '../src/editable'; import { draw, STYLES } from '../src/index'; -const { _center, _wireData, _indexes, _lastIndex } = exportedForTesting; +const { _center, _wireData, _equivOperation, _equivOperationParent, _indexes, _lastIndex } = exportedForTesting; describe('Test _center', () => { test('should return {25,50}', () => { @@ -90,6 +90,188 @@ describe('Test _wireData', () => { }); }); +describe('Test _equivOperation', () => { + const circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + test('should return H gate', () => { + expect(_equivOperation('0', circuit.operations)).toMatchSnapshot(); + }); + test('should return X gate', () => { + expect(_equivOperation('1', circuit.operations)).toMatchSnapshot(); + }); +}); + +describe('Test _equivOperationParent', () => { + test('should return Foo', () => { + const circuit = { + qubits: [{ id: 0, numChildren: 1 }, { id: 1 }, { id: 2 }, { id: 3 }], + operations: [ + { + gate: 'Foo', + conditionalRender: 3, + targets: [{ qId: 0 }, { qId: 1 }], + children: [ + { + gate: 'H', + targets: [{ qId: 1 }], + }, + { + gate: 'RX', + displayArgs: '(0.25)', + isControlled: true, + controls: [{ qId: 1 }], + targets: [{ qId: 0 }], + }, + ], + }, + { + gate: 'X', + targets: [{ qId: 3 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 1 }], + targets: [{ qId: 2 }, { qId: 3 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 2 }, { qId: 3 }], + targets: [{ qId: 1 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 1 }, { qId: 3 }], + targets: [{ qId: 2 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'measure', + isMeasurement: true, + controls: [{ qId: 0 }], + targets: [{ type: 1, qId: 0, cId: 0 }], + }, + { + gate: 'ApplyIfElseR', + isConditional: true, + controls: [{ type: 1, qId: 0, cId: 0 }], + targets: [], + children: [ + { + gate: 'H', + targets: [{ qId: 1 }], + conditionalRender: 1, + }, + { + gate: 'X', + targets: [{ qId: 1 }], + conditionalRender: 1, + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + conditionalRender: 2, + }, + { + gate: 'Foo', + targets: [{ qId: 3 }], + conditionalRender: 2, + }, + ], + }, + { + gate: 'SWAP', + targets: [{ qId: 0 }, { qId: 2 }], + children: [ + { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, + { gate: 'X', isControlled: true, controls: [{ qId: 2 }], targets: [{ qId: 0 }] }, + { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, + ], + }, + { + gate: 'ZZ', + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'ZZ', + targets: [{ qId: 0 }, { qId: 1 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 0 }, { qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + ], + }; + expect(_equivOperationParent('0-1', circuit.operations)).toMatchSnapshot(); + }); + test('should return all operations', () => { + const circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + expect(_equivOperationParent('0', circuit.operations)).toMatchSnapshot(); + }); +}); + describe('Test _lastIndex', () => { test('"" should return undefined', () => { expect(_lastIndex('')).toBeUndefined(); diff --git a/src/editable.ts b/src/editable.ts index 679de361..90129440 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -71,7 +71,7 @@ const _addDataWires = (container: HTMLElement) => { * i.e. Gate 'Foo' spans on wire 0 (y=40), 1 (y=100), and 2 (y=140). * Function returns [40, 100, 140] */ -const _elemWires = (elem: SVGGraphicsElement, wireData: number[]) => { +const _elemWireYs = (elem: SVGGraphicsElement, wireData: number[]) => { const { y, height } = elem.getBBox(); return wireData.filter((wireY) => wireY > y && wireY < y + height); }; @@ -129,9 +129,9 @@ const _dropzoneLayer = (context: Context) => { } else { // Let group gates creating dropzones for each wire const { x } = elem.getBBox(); - const elemWires = _elemWires(elem, wireData); + const elemWireYs = _elemWireYs(elem, wireData); - elemWires.map((wireY) => { + elemWireYs.map((wireY) => { const wirePrefix = wirePrefixes.find((item) => item.wireY === wireY); if (wirePrefix) { const prefixX = wirePrefix.prefixX; @@ -144,7 +144,6 @@ const _dropzoneLayer = (context: Context) => { dropzoneLayer.appendChild(elemDropzone); } }); - console.log({ elem, x, elemWires }); } }); @@ -390,6 +389,6 @@ const _renderFn = ( }; }; -const exportedForTesting = { _center, _wireData, _indexes, _lastIndex }; +const exportedForTesting = { _center, _wireData, _equivOperation, _equivOperationParent, _indexes, _lastIndex }; export { addEditable, exportedForTesting }; From 5daff513fa1ce9804a3ff374487443014c95d74e Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 28 Jun 2022 15:45:50 -0400 Subject: [PATCH 034/108] Add tests for _moveX, _copyX --- __tests__/__snapshots__/editable.test.ts.snap | 188 ++++++++++++++++++ __tests__/editable.test.ts | 75 ++++++- src/editable.ts | 19 +- 3 files changed, 275 insertions(+), 7 deletions(-) diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap index 6dd2e3a4..700b6219 100644 --- a/__tests__/__snapshots__/editable.test.ts.snap +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -1,5 +1,107 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Test _copyX copy elem from index 0 to index 1 1`] = ` +Array [ + Object { + "gate": "H", + "targets": Array [ + Object { + "qId": 0, + }, + ], + }, + Object { + "controls": Array [ + Object { + "qId": 0, + }, + ], + "gate": "X", + "isControlled": true, + "targets": Array [ + Object { + "qId": 1, + }, + ], + }, + Object { + "gate": "H", + "targets": Array [ + Object { + "qId": 0, + }, + ], + }, + Object { + "controls": Array [ + Object { + "qId": 1, + }, + ], + "gate": "Measure", + "isMeasurement": true, + "targets": Array [ + Object { + "cId": 0, + "qId": 1, + "type": 1, + }, + ], + }, +] +`; + +exports[`Test _copyX copy elem from index 0 to last 1`] = ` +Array [ + Object { + "gate": "H", + "targets": Array [ + Object { + "qId": 0, + }, + ], + }, + Object { + "controls": Array [ + Object { + "qId": 0, + }, + ], + "gate": "X", + "isControlled": true, + "targets": Array [ + Object { + "qId": 1, + }, + ], + }, + Object { + "controls": Array [ + Object { + "qId": 1, + }, + ], + "gate": "Measure", + "isMeasurement": true, + "targets": Array [ + Object { + "cId": 0, + "qId": 1, + "type": 1, + }, + ], + }, + Object { + "gate": "H", + "targets": Array [ + Object { + "qId": 0, + }, + ], + }, +] +`; + exports[`Test _equivOperation should return H gate 1`] = ` Object { "gate": "H", @@ -98,3 +200,89 @@ Array [ }, ] `; + +exports[`Test _moveX move elem from index 0 to index 1 1`] = ` +Array [ + Object { + "controls": Array [ + Object { + "qId": 0, + }, + ], + "gate": "X", + "isControlled": true, + "targets": Array [ + Object { + "qId": 1, + }, + ], + }, + Object { + "gate": "H", + "targets": Array [ + Object { + "qId": 0, + }, + ], + }, + Object { + "controls": Array [ + Object { + "qId": 1, + }, + ], + "gate": "Measure", + "isMeasurement": true, + "targets": Array [ + Object { + "cId": 0, + "qId": 1, + "type": 1, + }, + ], + }, +] +`; + +exports[`Test _moveX move elem from index 0 to last 1`] = ` +Array [ + Object { + "controls": Array [ + Object { + "qId": 0, + }, + ], + "gate": "X", + "isControlled": true, + "targets": Array [ + Object { + "qId": 1, + }, + ], + }, + Object { + "controls": Array [ + Object { + "qId": 1, + }, + ], + "gate": "Measure", + "isMeasurement": true, + "targets": Array [ + Object { + "cId": 0, + "qId": 1, + "type": 1, + }, + ], + }, + Object { + "gate": "H", + "targets": Array [ + Object { + "qId": 0, + }, + ], + }, +] +`; diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index 102fbc10..d8f38394 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -2,9 +2,10 @@ // Licensed under the MIT license. import { exportedForTesting } from '../src/editable'; -import { draw, STYLES } from '../src/index'; +import { Circuit, draw, STYLES } from '../src/index'; -const { _center, _wireData, _equivOperation, _equivOperationParent, _indexes, _lastIndex } = exportedForTesting; +const { _center, _wireData, _equivOperation, _equivOperationParent, _moveX, _copyX, _indexes, _lastIndex } = + exportedForTesting; describe('Test _center', () => { test('should return {25,50}', () => { @@ -272,6 +273,76 @@ describe('Test _equivOperationParent', () => { }); }); +describe('Test _moveX', () => { + let circuit: Circuit; + beforeEach(() => { + circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + }); + test('move elem from index 0 to index 1', () => { + _moveX('0', '2', circuit.operations); + expect(circuit.operations).toMatchSnapshot(); + }); + test('move elem from index 0 to last', () => { + _moveX('0', '3', circuit.operations); + expect(circuit.operations).toMatchSnapshot(); + }); +}); + +describe('Test _copyX', () => { + let circuit: Circuit; + beforeEach(() => { + circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + }); + test('copy elem from index 0 to index 1', () => { + _copyX('0', '2', circuit.operations); + expect(circuit.operations).toMatchSnapshot(); + }); + test('copy elem from index 0 to last', () => { + _copyX('0', '3', circuit.operations); + expect(circuit.operations).toMatchSnapshot(); + }); +}); + describe('Test _lastIndex', () => { test('"" should return undefined', () => { expect(_lastIndex('')).toBeUndefined(); diff --git a/src/editable.ts b/src/editable.ts index 90129440..d3639ab8 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -282,7 +282,7 @@ const _equivOperation = (dataId: string | null, operations: Operation[]): Operat return operationParent[index]; }; -const _moveX = (sourceId: string, targetId: string, operations: Operation[]) => { +const _moveX = (sourceId: string, targetId: string, operations: Operation[]): Operation | null => { if (sourceId === targetId) return _equivOperation(sourceId, operations); const sourceOperation = _equivOperation(sourceId, operations); const sourceOperationParent = _equivOperationParent(sourceId, operations); @@ -295,7 +295,7 @@ const _moveX = (sourceId: string, targetId: string, operations: Operation[]) => sourceOperation == null || sourceOperationParent == null ) - return; + return null; // Insert sourceOperation to target last index const newSourceOperation: Operation = { ...sourceOperation }; @@ -309,7 +309,7 @@ const _moveX = (sourceId: string, targetId: string, operations: Operation[]) => return newSourceOperation; }; -const _copyX = (sourceId: string, targetId: string, operations: Operation[]) => { +const _copyX = (sourceId: string, targetId: string, operations: Operation[]): Operation | null => { if (sourceId === targetId) return _equivOperation(sourceId, operations); const sourceOperation = _equivOperation(sourceId, operations); const sourceOperationParent = _equivOperationParent(sourceId, operations); @@ -322,7 +322,7 @@ const _copyX = (sourceId: string, targetId: string, operations: Operation[]) => sourceOperation == null || sourceOperationParent == null ) - return; + return null; // Insert sourceOperation to target last index const newSourceOperation: Operation = JSON.parse(JSON.stringify(sourceOperation)); @@ -389,6 +389,15 @@ const _renderFn = ( }; }; -const exportedForTesting = { _center, _wireData, _equivOperation, _equivOperationParent, _indexes, _lastIndex }; +const exportedForTesting = { + _center, + _wireData, + _equivOperation, + _equivOperationParent, + _moveX, + _copyX, + _indexes, + _lastIndex, +}; export { addEditable, exportedForTesting }; From 1c1d07379181951488a1d491df0abf9b3eb1706c Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 28 Jun 2022 18:02:23 -0400 Subject: [PATCH 035/108] Add tests for _circularMod --- __tests__/editable.test.ts | 25 +++++++++++++++++++++++-- src/editable.ts | 15 ++++++++------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index d8f38394..bc8de679 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -4,8 +4,17 @@ import { exportedForTesting } from '../src/editable'; import { Circuit, draw, STYLES } from '../src/index'; -const { _center, _wireData, _equivOperation, _equivOperationParent, _moveX, _copyX, _indexes, _lastIndex } = - exportedForTesting; +const { + _center, + _wireData, + _equivOperation, + _equivOperationParent, + _moveX, + _copyX, + _circularMod, + _indexes, + _lastIndex, +} = exportedForTesting; describe('Test _center', () => { test('should return {25,50}', () => { @@ -343,6 +352,18 @@ describe('Test _copyX', () => { }); }); +describe('Test _circularMod', () => { + test('should return 2', () => { + expect(_circularMod(5, 1, 4)).toEqual(2); + }); + test('should return 1', () => { + expect(_circularMod(100, 1, 2)).toEqual(1); + }); + test('should return 3', () => { + expect(_circularMod(3, 0, 4)).toEqual(3); + }); +}); + describe('Test _lastIndex', () => { test('"" should return undefined', () => { expect(_lastIndex('')).toBeUndefined(); diff --git a/src/editable.ts b/src/editable.ts index d3639ab8..8a78647a 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -336,28 +336,28 @@ const _moveY = (sourceWire: string, targetWire: string, operation: Operation, wi _offsetRecursively(offset, operation, wireData); }; -const _offsetRecursively = (offsetY: number, operation: Operation, wireData: number[]) => { +const _offsetRecursively = (offset: number, operation: Operation, wireData: number[]) => { const wireDataSize = wireData.length; // Offset all targets by offsetY value if (operation.targets != null) { operation.targets.forEach((target) => { - target.qId = _circularMod(target.qId, offsetY, wireDataSize); - if (target.cId) target.cId = _circularMod(target.cId, offsetY, wireDataSize); + target.qId = _circularMod(target.qId, offset, wireDataSize); + if (target.cId) target.cId = _circularMod(target.cId, offset, wireDataSize); }); } // Offset all controls by offsetY value if (operation.controls != null) { operation.controls.forEach((control) => { - control.qId = _circularMod(control.qId, offsetY, wireDataSize); - if (control.cId) control.cId = _circularMod(control.qId, offsetY, wireDataSize); + control.qId = _circularMod(control.qId, offset, wireDataSize); + if (control.cId) control.cId = _circularMod(control.qId, offset, wireDataSize); }); } // Offset recursively through all children if (operation.children != null) { - operation.children.forEach((child) => _offsetRecursively(offsetY, child, wireData)); + operation.children.forEach((child) => _offsetRecursively(offset, child, wireData)); } }; @@ -365,7 +365,7 @@ const _offsetRecursively = (offsetY: number, operation: Operation, wireData: num * This modulo function always returns positive value based on total. * i.e: value=0, offset=-1, total=4 returns 3 instead of -1 */ -const _circularMod = (value: number, offset: number, total: number) => { +const _circularMod = (value: number, offset: number, total: number): number => { return (((value + offset) % total) + total) % total; }; @@ -396,6 +396,7 @@ const exportedForTesting = { _equivOperationParent, _moveX, _copyX, + _circularMod, _indexes, _lastIndex, }; From 2b36d737f50f7bdfa3ad3154ad98be70303207d7 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 28 Jun 2022 18:18:23 -0400 Subject: [PATCH 036/108] Add tests for _offsetRecursively --- __tests__/editable.test.ts | 27 ++++++++++++++++++++++++++- src/editable.ts | 17 ++++++++--------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index bc8de679..ccdb33b5 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import { exportedForTesting } from '../src/editable'; -import { Circuit, draw, STYLES } from '../src/index'; +import { Circuit, draw, Operation, STYLES } from '../src/index'; const { _center, @@ -11,6 +11,7 @@ const { _equivOperationParent, _moveX, _copyX, + _offsetRecursively, _circularMod, _indexes, _lastIndex, @@ -352,6 +353,30 @@ describe('Test _copyX', () => { }); }); +describe('Test _offsetRecursively', () => { + let operation: Operation; + beforeEach(() => { + operation = { + gate: 'ZZ', + targets: [{ qId: 1 }, { qId: 3 }], + }; + }); + test('offset by 1', () => { + _offsetRecursively(operation, 1, 4); + expect(operation).toStrictEqual({ + gate: 'ZZ', + targets: [{ qId: 2 }, { qId: 0 }], + }); + }); + test('offset by 2', () => { + _offsetRecursively(operation, 2, 4); + expect(operation).toStrictEqual({ + gate: 'ZZ', + targets: [{ qId: 3 }, { qId: 1 }], + }); + }); +}); + describe('Test _circularMod', () => { test('should return 2', () => { expect(_circularMod(5, 1, 4)).toEqual(2); diff --git a/src/editable.ts b/src/editable.ts index 8a78647a..18c97c06 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -333,31 +333,29 @@ const _copyX = (sourceId: string, targetId: string, operations: Operation[]): Op const _moveY = (sourceWire: string, targetWire: string, operation: Operation, wireData: number[]) => { const offset = parseInt(targetWire) - parseInt(sourceWire); - _offsetRecursively(offset, operation, wireData); + _offsetRecursively(operation, offset, wireData.length); }; -const _offsetRecursively = (offset: number, operation: Operation, wireData: number[]) => { - const wireDataSize = wireData.length; - +const _offsetRecursively = (operation: Operation, wireOffset: number, totalWires: number): void => { // Offset all targets by offsetY value if (operation.targets != null) { operation.targets.forEach((target) => { - target.qId = _circularMod(target.qId, offset, wireDataSize); - if (target.cId) target.cId = _circularMod(target.cId, offset, wireDataSize); + target.qId = _circularMod(target.qId, wireOffset, totalWires); + if (target.cId) target.cId = _circularMod(target.cId, wireOffset, totalWires); }); } // Offset all controls by offsetY value if (operation.controls != null) { operation.controls.forEach((control) => { - control.qId = _circularMod(control.qId, offset, wireDataSize); - if (control.cId) control.cId = _circularMod(control.qId, offset, wireDataSize); + control.qId = _circularMod(control.qId, wireOffset, totalWires); + if (control.cId) control.cId = _circularMod(control.qId, wireOffset, totalWires); }); } // Offset recursively through all children if (operation.children != null) { - operation.children.forEach((child) => _offsetRecursively(offset, child, wireData)); + operation.children.forEach((child) => _offsetRecursively(child, wireOffset, totalWires)); } }; @@ -396,6 +394,7 @@ const exportedForTesting = { _equivOperationParent, _moveX, _copyX, + _offsetRecursively, _circularMod, _indexes, _lastIndex, From 6e2776c41b00f36a160f292c05e0625049822248 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 28 Jun 2022 18:27:06 -0400 Subject: [PATCH 037/108] Add tests for _moveY --- __tests__/editable.test.ts | 25 +++++++++++++++++++++++++ src/editable.ts | 12 ++++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index ccdb33b5..40244c02 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -11,6 +11,7 @@ const { _equivOperationParent, _moveX, _copyX, + _moveY, _offsetRecursively, _circularMod, _indexes, @@ -353,6 +354,30 @@ describe('Test _copyX', () => { }); }); +describe('Test _moveY', () => { + let operation: Operation; + beforeEach(() => { + operation = { + gate: 'ZZ', + targets: [{ qId: 1 }, { qId: 3 }], + }; + }); + test('offset by 1', () => { + _moveY('1', '2', operation, 4); + expect(operation).toStrictEqual({ + gate: 'ZZ', + targets: [{ qId: 2 }, { qId: 0 }], + }); + }); + test('offset by -3', () => { + _moveY('3', '0', operation, 4); + expect(operation).toStrictEqual({ + gate: 'ZZ', + targets: [{ qId: 2 }, { qId: 0 }], + }); + }); +}); + describe('Test _offsetRecursively', () => { let operation: Operation; beforeEach(() => { diff --git a/src/editable.ts b/src/editable.ts index 18c97c06..47b6e080 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -246,7 +246,7 @@ const _addEvents = (context: Context) => { : _moveX(context.selectedId, targetId, operations); if (newSourceOperation != null) { - _moveY(context.selectedWire, targetWire, newSourceOperation, context.wireData); + _moveY(context.selectedWire, targetWire, newSourceOperation, context.wireData.length); } renderFn(); @@ -331,12 +331,13 @@ const _copyX = (sourceId: string, targetId: string, operations: Operation[]): Op return newSourceOperation; }; -const _moveY = (sourceWire: string, targetWire: string, operation: Operation, wireData: number[]) => { +const _moveY = (sourceWire: string, targetWire: string, operation: Operation, totalWires: number): Operation => { const offset = parseInt(targetWire) - parseInt(sourceWire); - _offsetRecursively(operation, offset, wireData.length); + _offsetRecursively(operation, offset, totalWires); + return operation; }; -const _offsetRecursively = (operation: Operation, wireOffset: number, totalWires: number): void => { +const _offsetRecursively = (operation: Operation, wireOffset: number, totalWires: number): Operation => { // Offset all targets by offsetY value if (operation.targets != null) { operation.targets.forEach((target) => { @@ -357,6 +358,8 @@ const _offsetRecursively = (operation: Operation, wireOffset: number, totalWires if (operation.children != null) { operation.children.forEach((child) => _offsetRecursively(child, wireOffset, totalWires)); } + + return operation; }; /** @@ -394,6 +397,7 @@ const exportedForTesting = { _equivOperationParent, _moveX, _copyX, + _moveY, _offsetRecursively, _circularMod, _indexes, From ee00dc4eb1074e863aa6672c790e5caf4eef1209 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 28 Jun 2022 18:35:10 -0400 Subject: [PATCH 038/108] Add tests for _equivGateElem --- __tests__/__snapshots__/editable.test.ts.snap | 28 +++++++++++++++ __tests__/editable.test.ts | 34 +++++++++++++++++++ src/editable.ts | 1 + 3 files changed, 63 insertions(+) diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap index 700b6219..5655cf3a 100644 --- a/__tests__/__snapshots__/editable.test.ts.snap +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -102,6 +102,34 @@ Array [ ] `; +exports[`Test _equivGateElem should return gate H 1`] = ` + + + + + + H + + + + +`; + exports[`Test _equivOperation should return H gate 1`] = ` Object { "gate": "H", diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index 40244c02..d56ce7b1 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -7,6 +7,7 @@ import { Circuit, draw, Operation, STYLES } from '../src/index'; const { _center, _wireData, + _equivGateElem, _equivOperation, _equivOperationParent, _moveX, @@ -102,6 +103,39 @@ describe('Test _wireData', () => { }); }); +describe('Test _equivGateElem', () => { + let container: HTMLElement; + beforeAll(() => { + container = document.createElement('div'); + const circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + draw(circuit, container, STYLES['default']); + }); + test('should return gate H', () => { + const elem = container.querySelector('[class^="gate-"]') as SVGElement; + expect(_equivGateElem(elem)).toMatchSnapshot(); + }); +}); + describe('Test _equivOperation', () => { const circuit = { qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], diff --git a/src/editable.ts b/src/editable.ts index 47b6e080..2bf074f0 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -393,6 +393,7 @@ const _renderFn = ( const exportedForTesting = { _center, _wireData, + _equivGateElem, _equivOperation, _equivOperationParent, _moveX, From f63ecc36ad687d457b7fdecb49e3bbe2f3658554 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 28 Jun 2022 18:40:08 -0400 Subject: [PATCH 039/108] Add tests for _wirePrefixes --- __tests__/editable.test.ts | 17 +++++++++++++++++ src/editable.ts | 1 + 2 files changed, 18 insertions(+) diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index d56ce7b1..74d36ae8 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -5,6 +5,7 @@ import { exportedForTesting } from '../src/editable'; import { Circuit, draw, Operation, STYLES } from '../src/index'; const { + _wirePrefixes, _center, _wireData, _equivGateElem, @@ -19,6 +20,22 @@ const { _lastIndex, } = exportedForTesting; +describe('Test _wirePrefixes', () => { + test('2 wires', () => { + expect(_wirePrefixes([40, 100])).toStrictEqual([ + { index: 0, prefixX: 40, wireY: 40 }, + { index: 1, prefixX: 40, wireY: 100 }, + ]); + }); + test('3 wires', () => { + expect(_wirePrefixes([40, 100, 140])).toStrictEqual([ + { index: 0, prefixX: 40, wireY: 40 }, + { index: 1, prefixX: 40, wireY: 100 }, + { index: 2, prefixX: 40, wireY: 140 }, + ]); + }); +}); + describe('Test _center', () => { test('should return {25,50}', () => { Object.defineProperty(window.SVGElement.prototype, 'getBBox', { diff --git a/src/editable.ts b/src/editable.ts index 2bf074f0..52166aad 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -391,6 +391,7 @@ const _renderFn = ( }; const exportedForTesting = { + _wirePrefixes, _center, _wireData, _equivGateElem, From 100e2aa3e21423fff4bc6b2edbca59cf94a34ee5 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 28 Jun 2022 18:48:25 -0400 Subject: [PATCH 040/108] Add tests for _wireYs --- __tests__/editable.test.ts | 17 +++++++++++++++++ src/editable.ts | 12 +++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index 74d36ae8..b0123c40 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -5,6 +5,7 @@ import { exportedForTesting } from '../src/editable'; import { Circuit, draw, Operation, STYLES } from '../src/index'; const { + _wireYs, _wirePrefixes, _center, _wireData, @@ -20,6 +21,22 @@ const { _lastIndex, } = exportedForTesting; +describe('Test _wireYs', () => { + test('should return [40,100]', () => { + Object.defineProperty(window.SVGElement.prototype, 'getBBox', { + writable: true, + value: () => ({ + x: 0, + y: 20, + width: 0, + height: 120, + }), + }); + const elem = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + expect(_wireYs(elem, [40, 100, 140])).toStrictEqual([40, 100]); + }); +}); + describe('Test _wirePrefixes', () => { test('2 wires', () => { expect(_wirePrefixes([40, 100])).toStrictEqual([ diff --git a/src/editable.ts b/src/editable.ts index 52166aad..0f6b224b 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -71,7 +71,7 @@ const _addDataWires = (container: HTMLElement) => { * i.e. Gate 'Foo' spans on wire 0 (y=40), 1 (y=100), and 2 (y=140). * Function returns [40, 100, 140] */ -const _elemWireYs = (elem: SVGGraphicsElement, wireData: number[]) => { +const _wireYs = (elem: SVGGraphicsElement, wireData: number[]): number[] => { const { y, height } = elem.getBBox(); return wireData.filter((wireY) => wireY > y && wireY < y + height); }; @@ -82,7 +82,8 @@ const _hostElems = (container: HTMLElement) => { ); }; -const _wirePrefixes = (wireData: number[]) => wireData.map((wireY, index) => ({ index, wireY, prefixX: 40 })); +const _wirePrefixes = (wireData: number[]): { index: number; wireY: number; prefixX: number }[] => + wireData.map((wireY, index) => ({ index, wireY, prefixX: 40 })); /** * Find center point of element @@ -129,9 +130,9 @@ const _dropzoneLayer = (context: Context) => { } else { // Let group gates creating dropzones for each wire const { x } = elem.getBBox(); - const elemWireYs = _elemWireYs(elem, wireData); + const wireYs = _wireYs(elem, wireData); - elemWireYs.map((wireY) => { + wireYs.map((wireY) => { const wirePrefix = wirePrefixes.find((item) => item.wireY === wireY); if (wirePrefix) { const prefixX = wirePrefix.prefixX; @@ -177,7 +178,7 @@ const _wireData = (container: HTMLElement): number[] => { /** * Find equivalent gate element of host element */ -const _equivGateElem = (elem: SVGElement) => { +const _equivGateElem = (elem: SVGElement): SVGElement | null => { return elem.closest('[data-id]'); }; @@ -391,6 +392,7 @@ const _renderFn = ( }; const exportedForTesting = { + _wireYs, _wirePrefixes, _center, _wireData, From 47fc810cbfd93ecefc52dbb109a95fd3eab79abe Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 28 Jun 2022 18:58:36 -0400 Subject: [PATCH 041/108] Add tests for _hostElems --- __tests__/__snapshots__/editable.test.ts.snap | 46 +++++++++++++++++++ __tests__/editable.test.ts | 34 ++++++++++++++ src/editable.ts | 9 ++-- 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap index 5655cf3a..48af65d1 100644 --- a/__tests__/__snapshots__/editable.test.ts.snap +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -229,6 +229,52 @@ Array [ ] `; +exports[`Test _hostElems should return 4 elements 1`] = ` +Array [ + , + , + + + + + , + , +] +`; + exports[`Test _moveX move elem from index 0 to index 1 1`] = ` Array [ Object { diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index b0123c40..0bde57d6 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -6,6 +6,7 @@ import { Circuit, draw, Operation, STYLES } from '../src/index'; const { _wireYs, + _hostElems, _wirePrefixes, _center, _wireData, @@ -21,6 +22,39 @@ const { _lastIndex, } = exportedForTesting; +describe('Test _hostElems', () => { + let container: HTMLElement; + beforeAll(() => { + container = document.createElement('div'); + const circuit = { + qubits: [{ id: 0 }, { id: 1, numChildren: 1 }], + operations: [ + { + gate: 'H', + targets: [{ qId: 0 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + }, + { + gate: 'Measure', + isMeasurement: true, + controls: [{ qId: 1 }], + targets: [{ type: 1, qId: 1, cId: 0 }], + }, + ], + }; + draw(circuit, container, STYLES['default']); + }); + test('should return 4 elements', () => { + expect(_hostElems(container)).toMatchSnapshot(); + expect(_hostElems(container)).toHaveLength(4); + }); +}); + describe('Test _wireYs', () => { test('should return [40,100]', () => { Object.defineProperty(window.SVGElement.prototype, 'getBBox', { diff --git a/src/editable.ts b/src/editable.ts index 0f6b224b..fd863fb0 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -76,9 +76,11 @@ const _wireYs = (elem: SVGGraphicsElement, wireData: number[]): number[] => { return wireData.filter((wireY) => wireY > y && wireY < y + height); }; -const _hostElems = (container: HTMLElement) => { - return container.querySelectorAll( - '[class^="gate-"]:not(.gate-control, .gate-swap), .control-dot, .oplus, .cross', +const _hostElems = (container: HTMLElement): SVGGraphicsElement[] => { + return Array.from( + container.querySelectorAll( + '[class^="gate-"]:not(.gate-control, .gate-swap), .control-dot, .oplus, .cross', + ), ); }; @@ -393,6 +395,7 @@ const _renderFn = ( const exportedForTesting = { _wireYs, + _hostElems, _wirePrefixes, _center, _wireData, From cba0761f398890bea789219ca67193a4c6a02294 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 28 Jun 2022 20:12:19 -0400 Subject: [PATCH 042/108] Remove unnecessary condition when copying --- src/editable.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/editable.ts b/src/editable.ts index fd863fb0..46fd99b8 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -313,7 +313,6 @@ const _moveX = (sourceId: string, targetId: string, operations: Operation[]): Op }; const _copyX = (sourceId: string, targetId: string, operations: Operation[]): Operation | null => { - if (sourceId === targetId) return _equivOperation(sourceId, operations); const sourceOperation = _equivOperation(sourceId, operations); const sourceOperationParent = _equivOperationParent(sourceId, operations); const targetOperationParent = _equivOperationParent(targetId, operations); From 9fbe5669f961b3eb8e870b3b9353e048b72e56a3 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 28 Jun 2022 20:17:54 -0400 Subject: [PATCH 043/108] Clone operation when moving --- src/editable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editable.ts b/src/editable.ts index 46fd99b8..a18df3be 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -301,7 +301,7 @@ const _moveX = (sourceId: string, targetId: string, operations: Operation[]): Op return null; // Insert sourceOperation to target last index - const newSourceOperation: Operation = { ...sourceOperation }; + const newSourceOperation: Operation = JSON.parse(JSON.stringify(sourceOperation)); targetOperationParent.splice(targetLastIndex, 0, newSourceOperation); // Delete sourceOperation From cca0b1ca8b0d637b1975dfd2f984dc81d99972a6 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 28 Jun 2022 20:34:06 -0400 Subject: [PATCH 044/108] Reformat comments --- src/editable.ts | 90 ++++++++++++++++++++++++++++++++++++++++--------- src/styles.ts | 9 +++++ 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/editable.ts b/src/editable.ts index a18df3be..71b95595 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -39,15 +39,17 @@ const addEditable = (container: HTMLElement, sqore: Sqore, onCircuitChange?: (ci selectedWire: null, }; - // addDropzones(container); - // addDocumentEvents(container); + _addStyles(container, _wireData(container)); _addDataWires(container); svg.appendChild(_dropzoneLayer(context)); - // addDropzoneEvents(context); - // addMouseEvents(context); _addEvents(context); }; +/** + * Add data-wire to all host elements + * + * @param container HTML element for rendering visualization into + */ const _addDataWires = (container: HTMLElement) => { const elems = _hostElems(container); elems.forEach((elem) => { @@ -67,8 +69,8 @@ const _addDataWires = (container: HTMLElement) => { }; /** - * Create a list of wires that element is spanning on. - * i.e. Gate 'Foo' spans on wire 0 (y=40), 1 (y=100), and 2 (y=140). + * Create a list of wires that element is spanning on + * i.e. Gate 'Foo' spans on wire 0 (y=40), 1 (y=100), and 2 (y=140) * Function returns [40, 100, 140] */ const _wireYs = (elem: SVGGraphicsElement, wireData: number[]): number[] => { @@ -84,11 +86,20 @@ const _hostElems = (container: HTMLElement): SVGGraphicsElement[] => { ); }; +const _addStyles = (container: HTMLElement, wireData: number[]): void => { + const elems = _hostElems(container); + elems.forEach((elem) => { + if (_wireYs(elem, wireData).length < 2) elem.style.cursor = 'grab'; + }); +}; + const _wirePrefixes = (wireData: number[]): { index: number; wireY: number; prefixX: number }[] => wireData.map((wireY, index) => ({ index, wireY, prefixX: 40 })); /** - * Find center point of element + * Find center point of element + * + * @param elem Host element */ const _center = (elem: SVGGraphicsElement): { cX: number; cY: number } => { const { x, y, width, height } = elem.getBBox(); @@ -96,7 +107,9 @@ const _center = (elem: SVGGraphicsElement): { cX: number; cY: number } => { }; /** - * Create dropzone layer with all dropzones popullated + * Create dropzone layer with all dropzones popullated + * + * @param context Context object */ const _dropzoneLayer = (context: Context) => { const dropzoneLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g'); @@ -178,14 +191,18 @@ const _wireData = (container: HTMLElement): number[] => { }; /** - * Find equivalent gate element of host element + * Find equivalent gate element of host element + * + * @param elem Host element */ const _equivGateElem = (elem: SVGElement): SVGElement | null => { return elem.closest('[data-id]'); }; /** - * Find data-id of host element + * Find data-id of host element + * + * @param elem Host element */ const _equivDataId = (elem: SVGElement) => { const gateElem = _equivGateElem(elem); @@ -193,7 +210,9 @@ const _equivDataId = (elem: SVGElement) => { }; /** - * Disable contextmenu default behaviors + * Disable contextmenu default behaviors + * + * @param container HTML element for rendering visualization into */ const _addContextMenuEvents = (container: HTMLElement) => { container.addEventListener('contextmenu', (ev: MouseEvent) => { @@ -202,24 +221,60 @@ const _addContextMenuEvents = (container: HTMLElement) => { }; /** - * Add events specifically for dropzoneLayer + * Add events specifically for dropzoneLayer + * + * @param container HTML element for rendering visualization into + * @param dropzoneLayer SVG group element representing dropzone layer */ const _addDropzoneLayerEvents = (container: HTMLElement, dropzoneLayer: SVGGElement) => { container.addEventListener('mouseup', () => (dropzoneLayer.style.display = 'none')); }; +/** + * Add events for document + * + * @param context Context object + */ +const _addDocumentEvents = (context: Context) => { + const { container } = context; + + document.addEventListener('keydown', (ev: KeyboardEvent) => { + if (ev.ctrlKey && context.selectedId) { + container.classList.remove('moving'); + container.classList.add('copying'); + } + }); + + document.addEventListener('keyup', () => { + if (context.selectedId) { + container.classList.remove('moving'); + } + }); + + document.addEventListener('mouseup', () => { + container.classList.remove('moving', 'copying'); + }); +}; + +/** + * Add all events + * + * @param context Context object + */ const _addEvents = (context: Context) => { const { container, operations, renderFn } = context; const dropzoneLayer = container.querySelector('.dropzone-layer') as SVGGElement; _addContextMenuEvents(container); _addDropzoneLayerEvents(container, dropzoneLayer); + _addDocumentEvents(context); // Host element events const elems = _hostElems(container); elems.forEach((elem) => { elem.addEventListener('mousedown', () => { context.selectedWire = elem.getAttribute('data-wire'); + container.classList.add('moving'); }); const gateElem = _equivGateElem(elem); @@ -334,8 +389,10 @@ const _copyX = (sourceId: string, targetId: string, operations: Operation[]): Op }; const _moveY = (sourceWire: string, targetWire: string, operation: Operation, totalWires: number): Operation => { - const offset = parseInt(targetWire) - parseInt(sourceWire); - _offsetRecursively(operation, offset, totalWires); + if (operation.gate !== 'measure') { + const offset = parseInt(targetWire) - parseInt(sourceWire); + _offsetRecursively(operation, offset, totalWires); + } return operation; }; @@ -365,8 +422,9 @@ const _offsetRecursively = (operation: Operation, wireOffset: number, totalWires }; /** - * This modulo function always returns positive value based on total. - * i.e: value=0, offset=-1, total=4 returns 3 instead of -1 + * This modulo function always returns positive value based on total + * + * i.e: value=0, offset=-1, total=4 returns 3 instead of -1 */ const _circularMod = (value: number, offset: number, total: number): number => { return (((value + offset) % total) + total) % total; diff --git a/src/styles.ts b/src/styles.ts index 481aea8c..06e7d787 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -252,4 +252,13 @@ const _editable = ` fill: #EC7063; fill-opacity: 50%; } + .grab { + cursor: grab; + } + .moving { + cursor: move; + } + .copying { + cursor: copy; + } `; From 0347a80f14ffbe04f8da72d4adc6d5d3871d934c Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Tue, 28 Jun 2022 20:35:32 -0400 Subject: [PATCH 045/108] Reformat comments --- src/editable.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/editable.ts b/src/editable.ts index 71b95595..1168f878 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -49,6 +49,7 @@ const addEditable = (container: HTMLElement, sqore: Sqore, onCircuitChange?: (ci * Add data-wire to all host elements * * @param container HTML element for rendering visualization into + * */ const _addDataWires = (container: HTMLElement) => { const elems = _hostElems(container); @@ -72,6 +73,7 @@ const _addDataWires = (container: HTMLElement) => { * Create a list of wires that element is spanning on * i.e. Gate 'Foo' spans on wire 0 (y=40), 1 (y=100), and 2 (y=140) * Function returns [40, 100, 140] + * */ const _wireYs = (elem: SVGGraphicsElement, wireData: number[]): number[] => { const { y, height } = elem.getBBox(); @@ -100,6 +102,7 @@ const _wirePrefixes = (wireData: number[]): { index: number; wireY: number; pref * Find center point of element * * @param elem Host element + * */ const _center = (elem: SVGGraphicsElement): { cX: number; cY: number } => { const { x, y, width, height } = elem.getBBox(); @@ -110,6 +113,7 @@ const _center = (elem: SVGGraphicsElement): { cX: number; cY: number } => { * Create dropzone layer with all dropzones popullated * * @param context Context object + * */ const _dropzoneLayer = (context: Context) => { const dropzoneLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g'); @@ -194,6 +198,7 @@ const _wireData = (container: HTMLElement): number[] => { * Find equivalent gate element of host element * * @param elem Host element + * */ const _equivGateElem = (elem: SVGElement): SVGElement | null => { return elem.closest('[data-id]'); @@ -213,6 +218,7 @@ const _equivDataId = (elem: SVGElement) => { * Disable contextmenu default behaviors * * @param container HTML element for rendering visualization into + * */ const _addContextMenuEvents = (container: HTMLElement) => { container.addEventListener('contextmenu', (ev: MouseEvent) => { @@ -225,6 +231,7 @@ const _addContextMenuEvents = (container: HTMLElement) => { * * @param container HTML element for rendering visualization into * @param dropzoneLayer SVG group element representing dropzone layer + * */ const _addDropzoneLayerEvents = (container: HTMLElement, dropzoneLayer: SVGGElement) => { container.addEventListener('mouseup', () => (dropzoneLayer.style.display = 'none')); @@ -234,6 +241,7 @@ const _addDropzoneLayerEvents = (container: HTMLElement, dropzoneLayer: SVGGElem * Add events for document * * @param context Context object + * */ const _addDocumentEvents = (context: Context) => { const { container } = context; @@ -260,6 +268,7 @@ const _addDocumentEvents = (context: Context) => { * Add all events * * @param context Context object + * */ const _addEvents = (context: Context) => { const { container, operations, renderFn } = context; @@ -425,6 +434,7 @@ const _offsetRecursively = (operation: Operation, wireOffset: number, totalWires * This modulo function always returns positive value based on total * * i.e: value=0, offset=-1, total=4 returns 3 instead of -1 + * */ const _circularMod = (value: number, offset: number, total: number): number => { return (((value + offset) % total) + total) % total; From 69dedb215c614995662fafffe1e720c14ed42917 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Wed, 29 Jun 2022 12:58:39 -0400 Subject: [PATCH 046/108] Fix keyup behaviors --- src/editable.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/editable.ts b/src/editable.ts index 1168f878..49405309 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -208,6 +208,7 @@ const _equivGateElem = (elem: SVGElement): SVGElement | null => { * Find data-id of host element * * @param elem Host element + * */ const _equivDataId = (elem: SVGElement) => { const gateElem = _equivGateElem(elem); @@ -255,12 +256,15 @@ const _addDocumentEvents = (context: Context) => { document.addEventListener('keyup', () => { if (context.selectedId) { + container.classList.remove('copying'); container.classList.remove('moving'); } }); document.addEventListener('mouseup', () => { container.classList.remove('moving', 'copying'); + context.selectedId = null; + context.selectedWire = null; }); }; From bc94aff5685773f4a1ba75883d7d0acc087787f3 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Wed, 29 Jun 2022 13:10:34 -0400 Subject: [PATCH 047/108] Overhaul comments --- src/editable.ts | 82 +++++++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/src/editable.ts b/src/editable.ts index 49405309..05ac4522 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -22,9 +22,7 @@ interface Context { * @param container HTML element for rendering visualization into. * @param sqore Sqore object * @param onCircuitChange User-provided callback function triggered when circuit is changed - * */ - const addEditable = (container: HTMLElement, sqore: Sqore, onCircuitChange?: (circuit: Circuit) => void): void => { const svg = container.querySelector('svg') as SVGElement; @@ -47,9 +45,6 @@ const addEditable = (container: HTMLElement, sqore: Sqore, onCircuitChange?: (ci /** * Add data-wire to all host elements - * - * @param container HTML element for rendering visualization into - * */ const _addDataWires = (container: HTMLElement) => { const elems = _hostElems(container); @@ -70,16 +65,18 @@ const _addDataWires = (container: HTMLElement) => { }; /** - * Create a list of wires that element is spanning on - * i.e. Gate 'Foo' spans on wire 0 (y=40), 1 (y=100), and 2 (y=140) - * Function returns [40, 100, 140] - * + * Create a list of wires that element is spanning on + * i.e. Gate 'Foo' spans on wire 0 (y=40), 1 (y=100), and 2 (y=140) + * Function returns [40, 100, 140] */ const _wireYs = (elem: SVGGraphicsElement, wireData: number[]): number[] => { const { y, height } = elem.getBBox(); return wireData.filter((wireY) => wireY > y && wireY < y + height); }; +/** + * Get list of host elements that dropzones can be attached to + */ const _hostElems = (container: HTMLElement): SVGGraphicsElement[] => { return Array.from( container.querySelectorAll( @@ -88,6 +85,9 @@ const _hostElems = (container: HTMLElement): SVGGraphicsElement[] => { ); }; +/** + * Add custom styles specific to this module + */ const _addStyles = (container: HTMLElement, wireData: number[]): void => { const elems = _hostElems(container); elems.forEach((elem) => { @@ -95,14 +95,14 @@ const _addStyles = (container: HTMLElement, wireData: number[]): void => { }); }; +/** + * Generate an array of wire prefixes from wire data + */ const _wirePrefixes = (wireData: number[]): { index: number; wireY: number; prefixX: number }[] => wireData.map((wireY, index) => ({ index, wireY, prefixX: 40 })); /** * Find center point of element - * - * @param elem Host element - * */ const _center = (elem: SVGGraphicsElement): { cX: number; cY: number } => { const { x, y, width, height } = elem.getBBox(); @@ -111,9 +111,6 @@ const _center = (elem: SVGGraphicsElement): { cX: number; cY: number } => { /** * Create dropzone layer with all dropzones popullated - * - * @param context Context object - * */ const _dropzoneLayer = (context: Context) => { const dropzoneLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g'); @@ -180,6 +177,9 @@ const _dropzoneLayer = (context: Context) => { return dropzoneLayer; }; +/** + * Generate an array of y values based on circuit wires + */ const _wireData = (container: HTMLElement): number[] => { // elems include qubit wires and lines of measure gates const elems = container.querySelectorAll('svg > g:nth-child(3) > g'); @@ -196,9 +196,6 @@ const _wireData = (container: HTMLElement): number[] => { /** * Find equivalent gate element of host element - * - * @param elem Host element - * */ const _equivGateElem = (elem: SVGElement): SVGElement | null => { return elem.closest('[data-id]'); @@ -206,9 +203,6 @@ const _equivGateElem = (elem: SVGElement): SVGElement | null => { /** * Find data-id of host element - * - * @param elem Host element - * */ const _equivDataId = (elem: SVGElement) => { const gateElem = _equivGateElem(elem); @@ -217,9 +211,6 @@ const _equivDataId = (elem: SVGElement) => { /** * Disable contextmenu default behaviors - * - * @param container HTML element for rendering visualization into - * */ const _addContextMenuEvents = (container: HTMLElement) => { container.addEventListener('contextmenu', (ev: MouseEvent) => { @@ -229,10 +220,6 @@ const _addContextMenuEvents = (container: HTMLElement) => { /** * Add events specifically for dropzoneLayer - * - * @param container HTML element for rendering visualization into - * @param dropzoneLayer SVG group element representing dropzone layer - * */ const _addDropzoneLayerEvents = (container: HTMLElement, dropzoneLayer: SVGGElement) => { container.addEventListener('mouseup', () => (dropzoneLayer.style.display = 'none')); @@ -240,9 +227,6 @@ const _addDropzoneLayerEvents = (container: HTMLElement, dropzoneLayer: SVGGElem /** * Add events for document - * - * @param context Context object - * */ const _addDocumentEvents = (context: Context) => { const { container } = context; @@ -270,9 +254,6 @@ const _addDocumentEvents = (context: Context) => { /** * Add all events - * - * @param context Context object - * */ const _addEvents = (context: Context) => { const { container, operations, renderFn } = context; @@ -325,6 +306,9 @@ const _addEvents = (context: Context) => { }); }; +/** + * Find equivalent parent array of an operation + */ const _equivOperationParent = (dataId: string | null, operations: Operation[]): Operation[] | null => { if (!dataId) return null; @@ -338,6 +322,9 @@ const _equivOperationParent = (dataId: string | null, operations: Operation[]): return operationParent; }; +/** + * Find an equivalent operation of an element based on its data-id + */ const _equivOperation = (dataId: string | null, operations: Operation[]): Operation | null => { if (!dataId) return null; @@ -353,6 +340,9 @@ const _equivOperation = (dataId: string | null, operations: Operation[]): Operat return operationParent[index]; }; +/** + * Move an operation horizontally + */ const _moveX = (sourceId: string, targetId: string, operations: Operation[]): Operation | null => { if (sourceId === targetId) return _equivOperation(sourceId, operations); const sourceOperation = _equivOperation(sourceId, operations); @@ -380,6 +370,9 @@ const _moveX = (sourceId: string, targetId: string, operations: Operation[]): Op return newSourceOperation; }; +/** + * Copy an operation horizontally + */ const _copyX = (sourceId: string, targetId: string, operations: Operation[]): Operation | null => { const sourceOperation = _equivOperation(sourceId, operations); const sourceOperationParent = _equivOperationParent(sourceId, operations); @@ -401,6 +394,9 @@ const _copyX = (sourceId: string, targetId: string, operations: Operation[]): Op return newSourceOperation; }; +/** + * Move an operation vertically by changing its controls and targets + */ const _moveY = (sourceWire: string, targetWire: string, operation: Operation, totalWires: number): Operation => { if (operation.gate !== 'measure') { const offset = parseInt(targetWire) - parseInt(sourceWire); @@ -409,6 +405,9 @@ const _moveY = (sourceWire: string, targetWire: string, operation: Operation, to return operation; }; +/** + * Recursively change object controls and targets + */ const _offsetRecursively = (operation: Operation, wireOffset: number, totalWires: number): Operation => { // Offset all targets by offsetY value if (operation.targets != null) { @@ -436,23 +435,31 @@ const _offsetRecursively = (operation: Operation, wireOffset: number, totalWires /** * This modulo function always returns positive value based on total - * * i.e: value=0, offset=-1, total=4 returns 3 instead of -1 - * */ const _circularMod = (value: number, offset: number, total: number): number => { return (((value + offset) % total) + total) % total; }; +/** + * Split data-id into an array of indexes + */ const _indexes = (dataId: string): number[] => dataId !== '' // ? dataId.split('-').map((segment) => parseInt(segment)) : []; +/** + * Get the last index of data-id + * i.e: data-id = "0-1-2", _lastIndex will return 2 + */ const _lastIndex = (dataId: string): number | undefined => { return _indexes(dataId).pop(); }; +/** + * Return a render function with the onCircuitChange callback attached to it + */ const _renderFn = ( container: HTMLElement, sqore: Sqore, @@ -464,6 +471,9 @@ const _renderFn = ( }; }; +/** + * Object exported for unit testing + */ const exportedForTesting = { _wireYs, _hostElems, From 2f5e3f8c4973d9f2e4cd4dd8bc74c4297dd12ee6 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Thu, 30 Jun 2022 12:43:55 -0400 Subject: [PATCH 048/108] Fix add instead of remove cursor style --- src/editable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editable.ts b/src/editable.ts index 05ac4522..4c363d69 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -241,7 +241,7 @@ const _addDocumentEvents = (context: Context) => { document.addEventListener('keyup', () => { if (context.selectedId) { container.classList.remove('copying'); - container.classList.remove('moving'); + container.classList.add('moving'); } }); From 0019b5ec718397eb8f8f1eb13b73186e3071ca8e Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Wed, 6 Jul 2022 02:01:03 -0400 Subject: [PATCH 049/108] Rename _equivOperationParent to _equivParentArray --- __tests__/__snapshots__/editable.test.ts.snap | 4 ++-- __tests__/editable.test.ts | 8 ++++---- src/editable.ts | 20 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap index 48af65d1..9ea60e4d 100644 --- a/__tests__/__snapshots__/editable.test.ts.snap +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -158,7 +158,7 @@ Object { } `; -exports[`Test _equivOperationParent should return Foo 1`] = ` +exports[`Test _equivParentArray should return Foo 1`] = ` Array [ Object { "gate": "H", @@ -186,7 +186,7 @@ Array [ ] `; -exports[`Test _equivOperationParent should return all operations 1`] = ` +exports[`Test _equivParentArray should return all operations 1`] = ` Array [ Object { "gate": "H", diff --git a/__tests__/editable.test.ts b/__tests__/editable.test.ts index 0bde57d6..96ce98ec 100644 --- a/__tests__/editable.test.ts +++ b/__tests__/editable.test.ts @@ -12,7 +12,7 @@ const { _wireData, _equivGateElem, _equivOperation, - _equivOperationParent, + _equivParentArray, _moveX, _copyX, _moveY, @@ -234,7 +234,7 @@ describe('Test _equivOperation', () => { }); }); -describe('Test _equivOperationParent', () => { +describe('Test _equivParentArray', () => { test('should return Foo', () => { const circuit = { qubits: [{ id: 0, numChildren: 1 }, { id: 1 }, { id: 2 }, { id: 3 }], @@ -358,7 +358,7 @@ describe('Test _equivOperationParent', () => { }, ], }; - expect(_equivOperationParent('0-1', circuit.operations)).toMatchSnapshot(); + expect(_equivParentArray('0-1', circuit.operations)).toMatchSnapshot(); }); test('should return all operations', () => { const circuit = { @@ -382,7 +382,7 @@ describe('Test _equivOperationParent', () => { }, ], }; - expect(_equivOperationParent('0', circuit.operations)).toMatchSnapshot(); + expect(_equivParentArray('0', circuit.operations)).toMatchSnapshot(); }); }); diff --git a/src/editable.ts b/src/editable.ts index 4c363d69..3293cab3 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -309,17 +309,17 @@ const _addEvents = (context: Context) => { /** * Find equivalent parent array of an operation */ -const _equivOperationParent = (dataId: string | null, operations: Operation[]): Operation[] | null => { +const _equivParentArray = (dataId: string | null, operations: Operation[]): Operation[] | null => { if (!dataId) return null; const indexes = _indexes(dataId); indexes.pop(); - let operationParent = operations; + let parentArray = operations; for (const index of indexes) { - operationParent = operationParent[index].children || operationParent; + parentArray = parentArray[index].children || parentArray; } - return operationParent; + return parentArray; }; /** @@ -329,7 +329,7 @@ const _equivOperation = (dataId: string | null, operations: Operation[]): Operat if (!dataId) return null; const index = _lastIndex(dataId); - const operationParent = _equivOperationParent(dataId, operations); + const operationParent = _equivParentArray(dataId, operations); if ( operationParent == null || // @@ -346,8 +346,8 @@ const _equivOperation = (dataId: string | null, operations: Operation[]): Operat const _moveX = (sourceId: string, targetId: string, operations: Operation[]): Operation | null => { if (sourceId === targetId) return _equivOperation(sourceId, operations); const sourceOperation = _equivOperation(sourceId, operations); - const sourceOperationParent = _equivOperationParent(sourceId, operations); - const targetOperationParent = _equivOperationParent(targetId, operations); + const sourceOperationParent = _equivParentArray(sourceId, operations); + const targetOperationParent = _equivParentArray(targetId, operations); const targetLastIndex = _lastIndex(targetId); if ( @@ -375,8 +375,8 @@ const _moveX = (sourceId: string, targetId: string, operations: Operation[]): Op */ const _copyX = (sourceId: string, targetId: string, operations: Operation[]): Operation | null => { const sourceOperation = _equivOperation(sourceId, operations); - const sourceOperationParent = _equivOperationParent(sourceId, operations); - const targetOperationParent = _equivOperationParent(targetId, operations); + const sourceOperationParent = _equivParentArray(sourceId, operations); + const targetOperationParent = _equivParentArray(targetId, operations); const targetLastIndex = _lastIndex(targetId); if ( @@ -482,7 +482,7 @@ const exportedForTesting = { _wireData, _equivGateElem, _equivOperation, - _equivOperationParent, + _equivParentArray, _moveX, _copyX, _moveY, From eb1ea55e21d8f6d96432cceff95c60e58710b129 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Wed, 6 Jul 2022 03:34:04 -0400 Subject: [PATCH 050/108] Fix parent operation not updating to its children targets --- __tests__/__snapshots__/editable.test.ts.snap | 40 +++ __tests__/editable.test.ts | 276 ++++++++++++++++++ src/editable.ts | 64 ++++ 3 files changed, 380 insertions(+) diff --git a/__tests__/__snapshots__/editable.test.ts.snap b/__tests__/__snapshots__/editable.test.ts.snap index 9ea60e4d..c29c1bad 100644 --- a/__tests__/__snapshots__/editable.test.ts.snap +++ b/__tests__/__snapshots__/editable.test.ts.snap @@ -229,6 +229,46 @@ Array [ ] `; +exports[`Test _equivParentOperation should return Foo 1`] = ` +Object { + "children": Array [ + Object { + "gate": "H", + "targets": Array [ + Object { + "qId": 1, + }, + ], + }, + Object { + "controls": Array [ + Object { + "qId": 1, + }, + ], + "displayArgs": "(0.25)", + "gate": "RX", + "isControlled": true, + "targets": Array [ + Object { + "qId": 0, + }, + ], + }, + ], + "conditionalRender": 3, + "gate": "Foo", + "targets": Array [ + Object { + "qId": 0, + }, + Object { + "qId": 1, + }, + ], +} +`; + exports[`Test _hostElems should return 4 elements 1`] = ` Array [ { }); }); +describe('Test _equivParentOperation', () => { + test('should return Foo', () => { + const circuit = { + qubits: [{ id: 0, numChildren: 1 }, { id: 1 }, { id: 2 }, { id: 3 }], + operations: [ + { + gate: 'Foo', + conditionalRender: 3, + targets: [{ qId: 0 }, { qId: 1 }], + children: [ + { + gate: 'H', + targets: [{ qId: 1 }], + }, + { + gate: 'RX', + displayArgs: '(0.25)', + isControlled: true, + controls: [{ qId: 1 }], + targets: [{ qId: 0 }], + }, + ], + }, + { + gate: 'X', + targets: [{ qId: 3 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 1 }], + targets: [{ qId: 2 }, { qId: 3 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 2 }, { qId: 3 }], + targets: [{ qId: 1 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 1 }, { qId: 3 }], + targets: [{ qId: 2 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'measure', + isMeasurement: true, + controls: [{ qId: 0 }], + targets: [{ type: 1, qId: 0, cId: 0 }], + }, + { + gate: 'ApplyIfElseR', + isConditional: true, + controls: [{ type: 1, qId: 0, cId: 0 }], + targets: [], + children: [ + { + gate: 'H', + targets: [{ qId: 1 }], + conditionalRender: 1, + }, + { + gate: 'X', + targets: [{ qId: 1 }], + conditionalRender: 1, + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + conditionalRender: 2, + }, + { + gate: 'Foo', + targets: [{ qId: 3 }], + conditionalRender: 2, + }, + ], + }, + { + gate: 'SWAP', + targets: [{ qId: 0 }, { qId: 2 }], + children: [ + { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, + { gate: 'X', isControlled: true, controls: [{ qId: 2 }], targets: [{ qId: 0 }] }, + { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, + ], + }, + { + gate: 'ZZ', + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'ZZ', + targets: [{ qId: 0 }, { qId: 1 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 0 }, { qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + ], + }; + expect(_equivParentOperation('0-1', circuit.operations)).toMatchSnapshot(); + }); +}); + describe('Test _equivParentArray', () => { test('should return Foo', () => { const circuit = { @@ -504,6 +634,152 @@ describe('Test _offsetRecursively', () => { }); }); +describe('Test _targets', () => { + let circuit: Circuit; + beforeEach(() => { + circuit = { + qubits: [{ id: 0, numChildren: 1 }, { id: 1 }, { id: 2 }, { id: 3 }], + operations: [ + { + gate: 'Foo', + conditionalRender: 3, + targets: [{ qId: 0 }, { qId: 1 }], + children: [ + { + gate: 'H', + targets: [{ qId: 1 }], + }, + { + gate: 'RX', + displayArgs: '(0.25)', + isControlled: true, + controls: [{ qId: 1 }], + targets: [{ qId: 0 }], + }, + ], + }, + { + gate: 'X', + targets: [{ qId: 3 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 1 }], + targets: [{ qId: 2 }, { qId: 3 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 2 }, { qId: 3 }], + targets: [{ qId: 1 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 1 }, { qId: 3 }], + targets: [{ qId: 2 }], + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'measure', + isMeasurement: true, + controls: [{ qId: 0 }], + targets: [{ type: 1, qId: 0, cId: 0 }], + }, + { + gate: 'ApplyIfElseR', + isConditional: true, + controls: [{ type: 1, qId: 0, cId: 0 }], + targets: [], + children: [ + { + gate: 'H', + targets: [{ qId: 1 }], + conditionalRender: 1, + }, + { + gate: 'X', + targets: [{ qId: 1 }], + conditionalRender: 1, + }, + { + gate: 'X', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }], + conditionalRender: 2, + }, + { + gate: 'Foo', + targets: [{ qId: 3 }], + conditionalRender: 2, + }, + ], + }, + { + gate: 'SWAP', + targets: [{ qId: 0 }, { qId: 2 }], + children: [ + { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, + { gate: 'X', isControlled: true, controls: [{ qId: 2 }], targets: [{ qId: 0 }] }, + { gate: 'X', isControlled: true, controls: [{ qId: 0 }], targets: [{ qId: 2 }] }, + ], + }, + { + gate: 'ZZ', + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'ZZ', + targets: [{ qId: 0 }, { qId: 1 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 0 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + { + gate: 'XX', + isControlled: true, + controls: [{ qId: 0 }, { qId: 2 }], + targets: [{ qId: 1 }, { qId: 3 }], + }, + ], + }; + }); + test('move RX down 1, should return [{qId:1}, {qId:2}]', () => { + const parentOperation = circuit.operations[0]; + const parentArray = parentOperation.children; + if (parentArray) { + const operation = parentArray[1]; + _moveY('0', '1', operation, 4); + } + expect(_targets(parentOperation)).toStrictEqual([{ qId: 1 }, { qId: 2 }]); + }); + test('move RX down 2, should return [{qId:1}, {qId:2}, {qId:3}]', () => { + const parentOperation = circuit.operations[0]; + const parentArray = parentOperation.children; + if (parentArray) { + const operation = parentArray[1]; + _moveY('0', '2', operation, 4); + } + expect(_targets(parentOperation)).toStrictEqual([{ qId: 1 }, { qId: 2 }, { qId: 3 }]); + }); +}); + describe('Test _circularMod', () => { test('should return 2', () => { expect(_circularMod(5, 1, 4)).toEqual(2); diff --git a/src/editable.ts b/src/editable.ts index 3293cab3..d556217c 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -3,6 +3,7 @@ import { Circuit, Operation } from './circuit'; import { box } from './formatters/formatUtils'; +import { Register } from './register'; import { Sqore } from './sqore'; interface Context { @@ -299,6 +300,10 @@ const _addEvents = (context: Context) => { if (newSourceOperation != null) { _moveY(context.selectedWire, targetWire, newSourceOperation, context.wireData.length); + const parentOperation = _equivParentOperation(context.selectedId, operations); + if (parentOperation) { + parentOperation.targets = _targets(parentOperation); + } } renderFn(); @@ -306,6 +311,22 @@ const _addEvents = (context: Context) => { }); }; +const _equivParentOperation = (dataId: string | null, operations: Operation[]): Operation | null => { + if (!dataId) return null; + + const indexes = _indexes(dataId); + indexes.pop(); + const lastIndex = indexes.pop(); + + if (lastIndex == null) return null; + + let parentOperation = operations; + for (const index of indexes) { + parentOperation = parentOperation[index].children || parentOperation; + } + return parentOperation[lastIndex]; +}; + /** * Find equivalent parent array of an operation */ @@ -433,6 +454,47 @@ const _offsetRecursively = (operation: Operation, wireOffset: number, totalWires return operation; }; +/** + * Find targets of an operation by recursively walkthrough all of its children controls and targets + * i.e. Gate Foo contains gate H and gate RX. + * qIds of Gate H is 1 + * qIds of Gate RX is 1, 2 + * This should return [{qId: 1}, {qId: 2}] + */ +const _targets = (operation: Operation): Register[] | [] => { + const _recurse = (operation: Operation) => { + registers.push(...operation.targets); + if (operation.controls) { + registers.push(...operation.controls); + // If there is more children, keep adding more to registers + if (operation.children) { + for (const child of operation.children) { + _recurse(child); + } + } + } + }; + + const registers: Register[] = []; + if (operation.children == null) return []; + + // Recursively walkthrough all children to populate registers + for (const child of operation.children) { + _recurse(child); + } + + // Extract qIds from array of object + // i.e. [{qId: 0}, {qId: 1}, {qId: 1}] -> [0, 1, 1] + const qIds = registers.map((register) => register.qId); + const uniqueQIds = Array.from(new Set(qIds)); + + // Transform array of numbers into array of qId object + // i.e. -> [0, 1] -> [{qId: 0}, {qId: 1}, {qId: 1}] + return uniqueQIds.map((qId) => ({ + qId, + })); +}; + /** * This modulo function always returns positive value based on total * i.e: value=0, offset=-1, total=4 returns 3 instead of -1 @@ -482,11 +544,13 @@ const exportedForTesting = { _wireData, _equivGateElem, _equivOperation, + _equivParentOperation, _equivParentArray, _moveX, _copyX, _moveY, _offsetRecursively, + _targets, _circularMod, _indexes, _lastIndex, From 2fe5929eaf16668905efbfaa8f83c74edaedd211 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Thu, 7 Jul 2022 11:34:01 -0400 Subject: [PATCH 051/108] Fix comment --- src/editable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editable.ts b/src/editable.ts index d556217c..4c16c9cf 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -489,7 +489,7 @@ const _targets = (operation: Operation): Register[] | [] => { const uniqueQIds = Array.from(new Set(qIds)); // Transform array of numbers into array of qId object - // i.e. -> [0, 1] -> [{qId: 0}, {qId: 1}, {qId: 1}] + // i.e. [0, 1] -> [{qId: 0}, {qId: 1}] return uniqueQIds.map((qId) => ({ qId, })); From 352e74e9701ad2cf7998fff45bd7309718a837cf Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Thu, 7 Jul 2022 11:54:52 -0400 Subject: [PATCH 052/108] First commit new feature --- src/editable.ts | 6 +-- src/panel.ts | 124 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 3 deletions(-) create mode 100755 src/panel.ts diff --git a/src/editable.ts b/src/editable.ts index 4c16c9cf..8625f3b1 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -3,6 +3,7 @@ import { Circuit, Operation } from './circuit'; import { box } from './formatters/formatUtils'; +import { addPanel } from './panel'; import { Register } from './register'; import { Sqore } from './sqore'; @@ -38,6 +39,7 @@ const addEditable = (container: HTMLElement, sqore: Sqore, onCircuitChange?: (ci selectedWire: null, }; + addPanel(context); _addStyles(container, _wireData(container)); _addDataWires(container); svg.appendChild(_dropzoneLayer(context)); @@ -248,8 +250,6 @@ const _addDocumentEvents = (context: Context) => { document.addEventListener('mouseup', () => { container.classList.remove('moving', 'copying'); - context.selectedId = null; - context.selectedWire = null; }); }; @@ -556,4 +556,4 @@ const exportedForTesting = { _lastIndex, }; -export { addEditable, exportedForTesting }; +export { addEditable, Context, _equivOperation, exportedForTesting }; diff --git a/src/panel.ts b/src/panel.ts new file mode 100755 index 00000000..45405920 --- /dev/null +++ b/src/panel.ts @@ -0,0 +1,124 @@ +import { Operation } from './circuit'; +import { Context, _equivOperation } from './editable'; + +const addPanel = (context: Context): void => { + const { container } = context; + + container.prepend(_panel(operation)); +}; + +const _panel = (operation: Operation | null) => { + console.log(operation); + const options: Option[] = [ + { + value: '0', + text: 'q0', + }, + { + value: '1', + text: 'q1', + }, + { + value: '2', + text: 'q2', + }, + { + value: '3', + text: 'q3', + }, + ]; + + const panelElem = _elem('div'); + _children(panelElem, [ + _select('Target', 'target-input', options, 0), + _checkboxes('Controls', 'controls-input', options, [2, 3]), + _text('Display', 'display-input', 'display-arg'), + ]); + + return panelElem; +}; + +const _elem = (tag: string): HTMLElement => document.createElement(tag); + +/** + * Append all child elements to a parent element + */ +const _children = (parentElem: HTMLElement, childElems: HTMLElement[]) => { + childElems.map((elem) => parentElem.appendChild(elem)); + return parentElem; +}; + +interface Option { + value: string; + text: string; +} + +const _select = (label: string, className: string, options: Option[], selectedIndex: number) => { + const optionElems = options.map(({ value, text }) => _option(value, text)); + const selectElem = _elem('select') as HTMLSelectElement; + _children(selectElem, optionElems); + selectElem.selectedIndex = selectedIndex; + + const labelElem = _elem('label') as HTMLLabelElement; + labelElem.textContent = label; + + const divElem = _elem('div') as HTMLDivElement; + divElem.className = className; + _children(divElem, [labelElem, selectElem]); + + return divElem; +}; + +const _option = (value: string, text: string) => { + const elem = _elem('option') as HTMLOptionElement; + elem.value = value; + elem.textContent = text; + return elem; +}; + +const _checkboxes = (label: string, className: string, options: Option[], selectedIndexes: number[]) => { + const checkboxElems = options.map((option, index) => { + const elem = _checkbox(option.value, option.text); + if (selectedIndexes.includes(index)) { + elem.querySelector('input')?.setAttribute('checked', 'true'); + } + return elem; + }); + + const labelElem = _elem('label'); + labelElem.textContent = label; + + const divElem = _elem('div') as HTMLDivElement; + divElem.className = className; + _children(divElem, [labelElem, ...checkboxElems]); + + return divElem; +}; + +const _checkbox = (value: string, text: string) => { + const inputElem = _elem('input') as HTMLInputElement; + inputElem.type = 'checkbox'; + inputElem.value = value; + + const labelElem = _elem('label') as HTMLLabelElement; + labelElem.textContent = text; + labelElem.prepend(inputElem); + return labelElem; +}; + +const _text = (label: string, className: string, value: string) => { + const labelElem = _elem('label') as HTMLLabelElement; + labelElem.textContent = label; + + const textElem = _elem('input') as HTMLInputElement; + textElem.type = 'text'; + textElem.value = value; + + const divElem = _elem('div'); + divElem.className = className; + _children(divElem, [labelElem, textElem]); + + return divElem; +}; + +export { addPanel }; From 22ff44b2add344226d2a492872c356a8660c0ee3 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Thu, 7 Jul 2022 12:48:20 -0400 Subject: [PATCH 053/108] Remove Context dependency --- src/editable.ts | 2 +- src/panel.ts | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/editable.ts b/src/editable.ts index 8625f3b1..c89aa530 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -39,7 +39,7 @@ const addEditable = (container: HTMLElement, sqore: Sqore, onCircuitChange?: (ci selectedWire: null, }; - addPanel(context); + addPanel(container); _addStyles(container, _wireData(container)); _addDataWires(container); svg.appendChild(_dropzoneLayer(context)); diff --git a/src/panel.ts b/src/panel.ts index 45405920..7695f023 100755 --- a/src/panel.ts +++ b/src/panel.ts @@ -1,14 +1,8 @@ -import { Operation } from './circuit'; -import { Context, _equivOperation } from './editable'; - -const addPanel = (context: Context): void => { - const { container } = context; - - container.prepend(_panel(operation)); +const addPanel = (container: HTMLElement): void => { + container.prepend(_panel()); }; -const _panel = (operation: Operation | null) => { - console.log(operation); +const _panel = () => { const options: Option[] = [ { value: '0', From 16639dcf72bbfad7792c07593b5f4059d2ef5a2d Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Thu, 7 Jul 2022 18:59:44 -0400 Subject: [PATCH 054/108] Display data when gate is clicked --- src/editable.ts | 4 ++-- src/panel.ts | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/editable.ts b/src/editable.ts index c89aa530..e001ba16 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -39,7 +39,7 @@ const addEditable = (container: HTMLElement, sqore: Sqore, onCircuitChange?: (ci selectedWire: null, }; - addPanel(container); + addPanel(container, context.operations); _addStyles(container, _wireData(container)); _addDataWires(container); svg.appendChild(_dropzoneLayer(context)); @@ -301,7 +301,7 @@ const _addEvents = (context: Context) => { if (newSourceOperation != null) { _moveY(context.selectedWire, targetWire, newSourceOperation, context.wireData.length); const parentOperation = _equivParentOperation(context.selectedId, operations); - if (parentOperation) { + if (parentOperation != null) { parentOperation.targets = _targets(parentOperation); } } diff --git a/src/panel.ts b/src/panel.ts index 7695f023..d69ad201 100755 --- a/src/panel.ts +++ b/src/panel.ts @@ -1,5 +1,14 @@ -const addPanel = (container: HTMLElement): void => { +import { Operation } from './circuit'; +import { _equivOperation } from './editable'; + +const addPanel = (container: HTMLElement, operations: Operation[]): void => { container.prepend(_panel()); + container.querySelectorAll('[data-id]').forEach((elem) => + elem.addEventListener('mousedown', () => { + const dataId = elem.getAttribute('data-id'); + console.log({ dataId, operation: _equivOperation(dataId, operations) }); + }), + ); }; const _panel = () => { From d45089b1d404412c844052506411336a1136dc61 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen <37777232+aaronthangnguyen@users.noreply.github.com> Date: Thu, 7 Jul 2022 19:04:28 -0400 Subject: [PATCH 055/108] Sync to `main` (#73) * v 1.0.5 * Fixing jupyter viewer display (#70) Co-authored-by: Andres Paz --- package.json | 2 +- quantum-viz/pyproject.toml | 2 +- quantum-viz/quantum_viz/widget.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d9dbd88a..c138e1ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/quantum-viz.js", - "version": "1.0.4", + "version": "1.0.5", "description": "quantum-viz.js is a configurable tool for visualizing quantum circuits.", "main": "dist/qviz.min.js", "scripts": { diff --git a/quantum-viz/pyproject.toml b/quantum-viz/pyproject.toml index 2914cf61..960aff8e 100644 --- a/quantum-viz/pyproject.toml +++ b/quantum-viz/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "quantum-viz" -version = "1.0.4" +version = "1.0.5" description = "quantum-viz.js Python tools" authors = ["Microsoft Corporation "] license = "MIT" diff --git a/quantum-viz/quantum_viz/widget.py b/quantum-viz/quantum_viz/widget.py index 829a2a4d..3a33bba8 100644 --- a/quantum-viz/quantum_viz/widget.py +++ b/quantum-viz/quantum_viz/widget.py @@ -134,7 +134,7 @@ def html_str(self, uid: Optional[str] = None) -> str: def _ipython_display_(self) -> None: """Display the widget with IPython.""" viewer = HTML(self.html_str()) - display((viewer,)) + display(viewer) def browser_display(self) -> None: """Display the widget in the browser.""" From 48c21447ce3217852fde82b6fb18084ed874ee72 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Mon, 11 Jul 2022 15:36:24 -0400 Subject: [PATCH 056/108] Allow target edit --- package.json | 2 ++ src/editable.ts | 9 ++++---- src/index.ts | 2 ++ src/panel.ts | 59 +++++++++++++++++++++++++++---------------------- tsconfig.json | 1 + 5 files changed, 42 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index d9dbd88a..d103e7d4 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "license": "MIT", "devDependencies": { "@types/jest": "^26.0.4", + "@types/lodash": "^4.14.182", "@types/prettier": "2.6.0", "@typescript-eslint/eslint-plugin": "^3.9.0", "@typescript-eslint/parser": "^3.9.0", @@ -41,6 +42,7 @@ "eslint-plugin-jest": "^23.20.0", "eslint-plugin-prettier": "^3.1.4", "jest": "^26.6.3", + "lodash": "^4.17.21", "prettier": "2.6.0", "terser-webpack-plugin": "^4.1.0", "ts-jest": "^26.1.2", diff --git a/src/editable.ts b/src/editable.ts index e001ba16..f6e54776 100644 --- a/src/editable.ts +++ b/src/editable.ts @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +import cloneDeep from 'lodash/cloneDeep'; +import isEqual from 'lodash/isEqual'; import { Circuit, Operation } from './circuit'; import { box } from './formatters/formatUtils'; -import { addPanel } from './panel'; import { Register } from './register'; import { Sqore } from './sqore'; @@ -38,8 +39,6 @@ const addEditable = (container: HTMLElement, sqore: Sqore, onCircuitChange?: (ci selectedId: null, selectedWire: null, }; - - addPanel(container, context.operations); _addStyles(container, _wireData(container)); _addDataWires(container); svg.appendChild(_dropzoneLayer(context)); @@ -284,6 +283,7 @@ const _addEvents = (context: Context) => { const dropzoneElems = dropzoneLayer.querySelectorAll('.dropzone'); dropzoneElems.forEach((dropzoneElem) => { dropzoneElem.addEventListener('mouseup', (ev: MouseEvent) => { + const originalOperations = cloneDeep(operations); const targetId = dropzoneElem.getAttribute('data-dropzone-id'); const targetWire = dropzoneElem.getAttribute('data-dropzone-wire'); if ( @@ -305,8 +305,7 @@ const _addEvents = (context: Context) => { parentOperation.targets = _targets(parentOperation); } } - - renderFn(); + if (isEqual(originalOperations, operations) === false) renderFn(); }); }); }; diff --git a/src/index.ts b/src/index.ts index 56b35003..fdc0c9a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import { Sqore } from './sqore'; import { Circuit } from './circuit'; import { StyleConfig } from './styles'; +import { addPanel } from './panel'; /** * Render `circuit` into `container` at the specified layer depth. @@ -26,6 +27,7 @@ export const draw = ( const sqore = new Sqore(circuit, style); sqore.draw(container, renderDepth, isEditable, onCircuitChange); + addPanel(container, sqore); }; export { STYLES } from './styles'; diff --git a/src/panel.ts b/src/panel.ts index d69ad201..754759af 100755 --- a/src/panel.ts +++ b/src/panel.ts @@ -1,40 +1,34 @@ +import range from 'lodash/range'; import { Operation } from './circuit'; import { _equivOperation } from './editable'; +import { Sqore } from './sqore'; -const addPanel = (container: HTMLElement, operations: Operation[]): void => { - container.prepend(_panel()); - container.querySelectorAll('[data-id]').forEach((elem) => +const addPanel = (container: HTMLElement, sqore: Sqore): void => { + const elems = container.querySelectorAll('[data-id]'); + elems.forEach((elem) => elem.addEventListener('mousedown', () => { const dataId = elem.getAttribute('data-id'); - console.log({ dataId, operation: _equivOperation(dataId, operations) }); + const operation = _equivOperation(dataId, sqore.circuit.operations); + const newPanelElem = _panel(qubitSize, operation || undefined); + container.replaceChild(newPanelElem, panelElem); + panelElem = newPanelElem; }), ); + const qubitSize = sqore.circuit.qubits.length; + let panelElem = _panel(qubitSize); + container.prepend(panelElem); }; -const _panel = () => { - const options: Option[] = [ - { - value: '0', - text: 'q0', - }, - { - value: '1', - text: 'q1', - }, - { - value: '2', - text: 'q2', - }, - { - value: '3', - text: 'q3', - }, - ]; +const _panel = (qubitSize: number, operation?: Operation) => { + const options = range(qubitSize).map((i) => ({ value: `${i}`, text: `q${i}` })); + const target = operation?.targets[0].qId; + const controls = operation?.controls?.map((control) => control.qId); const panelElem = _elem('div'); + panelElem.className = 'panel'; _children(panelElem, [ - _select('Target', 'target-input', options, 0), - _checkboxes('Controls', 'controls-input', options, [2, 3]), + _select('Target', 'target-input', options, target || 0, operation), + _checkboxes('Controls', 'controls-input', options, controls || []), _text('Display', 'display-input', 'display-arg'), ]); @@ -56,7 +50,13 @@ interface Option { text: string; } -const _select = (label: string, className: string, options: Option[], selectedIndex: number) => { +const _select = ( + label: string, + className: string, + options: Option[], + selectedIndex: number, + operation?: Operation, +): HTMLElement => { const optionElems = options.map(({ value, text }) => _option(value, text)); const selectElem = _elem('select') as HTMLSelectElement; _children(selectElem, optionElems); @@ -69,6 +69,13 @@ const _select = (label: string, className: string, options: Option[], selectedIn divElem.className = className; _children(divElem, [labelElem, selectElem]); + selectElem.onchange = () => { + if (operation != null) { + Object.assign(operation.targets, [{ qId: selectElem.value }]); + console.log(operation); + } + }; + return divElem; }; diff --git a/tsconfig.json b/tsconfig.json index afc965bf..c3f66ab0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "declaration": true, "strict": true, "outDir": "lib", + "esModuleInterop": true }, "include": [ "src/**/*" From 123c86944eafea9e361c12d3aa28b4b77578b90c Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Mon, 11 Jul 2022 15:49:16 -0400 Subject: [PATCH 057/108] Change render mechanism --- src/sqore.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sqore.ts b/src/sqore.ts index 17934f74..b88db601 100644 --- a/src/sqore.ts +++ b/src/sqore.ts @@ -125,13 +125,13 @@ export class Sqore { // Create visualization components const composedSqore: ComposedSqore = this.compose(circuit); const svg: SVGElement = this.generateSvg(composedSqore); - container.innerHTML = ''; - container.appendChild(svg); + const previousSvg = container.querySelector('svg'); + previousSvg == null // + ? container.appendChild(svg) + : container.replaceChild(svg, previousSvg); this.addGateClickHandlers(container, circuit, isEditable, onCircuitChange); - if (isEditable) { - addEditable(container, this, onCircuitChange); - } + isEditable && addEditable(container, this, onCircuitChange); } /** From 863b7d27160e9338c4ec4e424ec1fb3aa41b9bc2 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Mon, 11 Jul 2022 16:27:38 -0400 Subject: [PATCH 058/108] use Reducer design pattern --- src/panel.ts | 51 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/src/panel.ts b/src/panel.ts index 754759af..7b20d16d 100755 --- a/src/panel.ts +++ b/src/panel.ts @@ -19,6 +19,26 @@ const addPanel = (container: HTMLElement, sqore: Sqore): void => { container.prepend(panelElem); }; +interface Action { + type: string; + payload: unknown; +} + +const reducer = (initial: Operation | undefined, action: Action) => { + if (initial == null) return; + + switch (action.type) { + case 'TARGET': { + Object.assign(initial, { ...initial, targets: action.payload }); + } + case 'CONTROLS': { + Object.assign(initial, { ...initial, controls: action.payload }); + } + } + console.log(initial); + console.log('Re-rendering...'); +}; + const _panel = (qubitSize: number, operation?: Operation) => { const options = range(qubitSize).map((i) => ({ value: `${i}`, text: `q${i}` })); const target = operation?.targets[0].qId; @@ -28,7 +48,7 @@ const _panel = (qubitSize: number, operation?: Operation) => { panelElem.className = 'panel'; _children(panelElem, [ _select('Target', 'target-input', options, target || 0, operation), - _checkboxes('Controls', 'controls-input', options, controls || []), + _checkboxes('Controls', 'controls-input', options, controls || [], operation), _text('Display', 'display-input', 'display-arg'), ]); @@ -60,6 +80,7 @@ const _select = ( const optionElems = options.map(({ value, text }) => _option(value, text)); const selectElem = _elem('select') as HTMLSelectElement; _children(selectElem, optionElems); + operation == null && selectElem.setAttribute('disabled', 'true'); selectElem.selectedIndex = selectedIndex; const labelElem = _elem('label') as HTMLLabelElement; @@ -70,10 +91,7 @@ const _select = ( _children(divElem, [labelElem, selectElem]); selectElem.onchange = () => { - if (operation != null) { - Object.assign(operation.targets, [{ qId: selectElem.value }]); - console.log(operation); - } + reducer(operation, { type: 'TARGET', payload: [{ qId: selectElem.value }] }); }; return divElem; @@ -86,12 +104,27 @@ const _option = (value: string, text: string) => { return elem; }; -const _checkboxes = (label: string, className: string, options: Option[], selectedIndexes: number[]) => { +const _checkboxes = ( + label: string, + className: string, + options: Option[], + selectedIndexes: number[], + operation?: Operation, +) => { const checkboxElems = options.map((option, index) => { const elem = _checkbox(option.value, option.text); - if (selectedIndexes.includes(index)) { - elem.querySelector('input')?.setAttribute('checked', 'true'); - } + const inputElem = elem.querySelector('input') as HTMLInputElement; + selectedIndexes.includes(index) && inputElem.setAttribute('checked', 'true'); + operation == null && inputElem.setAttribute('disabled', 'true'); + + inputElem.onchange = () => { + const checkedElems = Array.from(divElem.querySelectorAll('input:checked')); + const newControls = checkedElems.map((elem) => ({ + qId: elem.value, + })); + reducer(operation, { type: 'CONTROLS', payload: newControls }); + }; + return elem; }); From 90b29bc62cac43196cafdb94be0b04ad701d1ac3 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Mon, 11 Jul 2022 19:13:43 -0400 Subject: [PATCH 059/108] Add basic rendering --- src/panel.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/panel.ts b/src/panel.ts index 7b20d16d..7ffad9bd 100755 --- a/src/panel.ts +++ b/src/panel.ts @@ -9,13 +9,14 @@ const addPanel = (container: HTMLElement, sqore: Sqore): void => { elem.addEventListener('mousedown', () => { const dataId = elem.getAttribute('data-id'); const operation = _equivOperation(dataId, sqore.circuit.operations); - const newPanelElem = _panel(qubitSize, operation || undefined); + const newPanelElem = _panel(qubitSize, dispatch, operation || undefined); container.replaceChild(newPanelElem, panelElem); panelElem = newPanelElem; }), ); + const dispatch = reducer(container, sqore); const qubitSize = sqore.circuit.qubits.length; - let panelElem = _panel(qubitSize); + let panelElem = _panel(qubitSize, dispatch); container.prepend(panelElem); }; @@ -24,7 +25,7 @@ interface Action { payload: unknown; } -const reducer = (initial: Operation | undefined, action: Action) => { +const reducer = (container: HTMLElement, sqore: Sqore) => (initial: Operation | undefined, action: Action) => { if (initial == null) return; switch (action.type) { @@ -35,11 +36,10 @@ const reducer = (initial: Operation | undefined, action: Action) => { Object.assign(initial, { ...initial, controls: action.payload }); } } - console.log(initial); - console.log('Re-rendering...'); + sqore.draw(container); }; -const _panel = (qubitSize: number, operation?: Operation) => { +const _panel = (qubitSize: number, dispatch: Dispatch, operation?: Operation) => { const options = range(qubitSize).map((i) => ({ value: `${i}`, text: `q${i}` })); const target = operation?.targets[0].qId; const controls = operation?.controls?.map((control) => control.qId); @@ -47,8 +47,8 @@ const _panel = (qubitSize: number, operation?: Operation) => { const panelElem = _elem('div'); panelElem.className = 'panel'; _children(panelElem, [ - _select('Target', 'target-input', options, target || 0, operation), - _checkboxes('Controls', 'controls-input', options, controls || [], operation), + _select('Target', 'target-input', options, target || 0, dispatch, operation), + _checkboxes('Controls', 'controls-input', options, controls || [], dispatch, operation), _text('Display', 'display-input', 'display-arg'), ]); @@ -70,11 +70,16 @@ interface Option { text: string; } +interface Dispatch { + (initial: Operation | undefined, action: Action): void; +} + const _select = ( label: string, className: string, options: Option[], selectedIndex: number, + dispatch: Dispatch, operation?: Operation, ): HTMLElement => { const optionElems = options.map(({ value, text }) => _option(value, text)); @@ -91,7 +96,7 @@ const _select = ( _children(divElem, [labelElem, selectElem]); selectElem.onchange = () => { - reducer(operation, { type: 'TARGET', payload: [{ qId: selectElem.value }] }); + dispatch(operation, { type: 'TARGET', payload: [{ qId: selectElem.value }] }); }; return divElem; @@ -109,6 +114,7 @@ const _checkboxes = ( className: string, options: Option[], selectedIndexes: number[], + dispatch: Dispatch, operation?: Operation, ) => { const checkboxElems = options.map((option, index) => { @@ -122,7 +128,7 @@ const _checkboxes = ( const newControls = checkedElems.map((elem) => ({ qId: elem.value, })); - reducer(operation, { type: 'CONTROLS', payload: newControls }); + dispatch(operation, { type: 'CONTROLS', payload: newControls }); }; return elem; From 5b51043afd2c0ca8f8e2b9fd6944151ecf3241d9 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen <37777232+aaronthangnguyen@users.noreply.github.com> Date: Wed, 13 Jul 2022 15:12:26 -0400 Subject: [PATCH 060/108] Add extensions feature (#76) * First commit * Fix mutation * Fix persistence in panel * Allow editing displayArgs --- example/script.js | 26 ++++--- src/{editable.ts => draggable.ts} | 23 ++---- src/index.ts | 15 +--- src/panel.ts | 79 ++++++++++++++------- src/sqore.ts | 112 +++++++++++++++++------------- 5 files changed, 139 insertions(+), 116 deletions(-) rename src/{editable.ts => draggable.ts} (96%) diff --git a/example/script.js b/example/script.js index 9c15533c..d65eb0d7 100644 --- a/example/script.js +++ b/example/script.js @@ -7,30 +7,38 @@ if (typeof qviz != 'undefined') { /* These examples shows how to draw circuits into containers. */ const entangleDiv = document.getElementById('entangle'); if (entangleDiv != null) { - qviz.draw(entangle, entangleDiv, qviz.STYLES['Default'], 0, true); + qviz.create(entangle) // + .useDraggable() + .usePanel() + .draw(entangleDiv); } const sampleDiv = document.getElementById('sample'); if (sampleDiv != null) { - const isEditable = true; - const onCircuitChange = (circuit) => { - console.log('New circuit ↓'); - console.log(circuit); - }; /* Pass in isEditable = true to allow circuit to be editable */ /* Pass in onCircuitChange to trigger callback function whenever there is a change in circuit */ - qviz.draw(sample, sampleDiv, qviz.STYLES['Default'], 0, isEditable, onCircuitChange); + // qviz.draw(sample, sampleDiv, qviz.STYLES['Default'], 0, isEditable, onCircuitChange); + qviz.create(sample) // + .useDraggable() + .usePanel() + .useOnCircuitChange((circuit) => { + console.log('New circuit ↓'); + console.log(circuit); + }) + .draw(sampleDiv); } const teleportDiv = document.getElementById('teleport'); if (teleportDiv != null) { - qviz.draw(teleport, teleportDiv, qviz.STYLES['Default'], 0, true); + qviz.create(teleport) // + .draw(teleportDiv); } const groverDiv = document.getElementById('grover'); if (groverDiv != null) { - qviz.draw(grover, groverDiv, qviz.STYLES['Default']); + qviz.create(grover) // + .draw(groverDiv); } } else { document.getElementById('group').remove(); diff --git a/src/editable.ts b/src/draggable.ts similarity index 96% rename from src/editable.ts rename to src/draggable.ts index f6e54776..5ab8c854 100644 --- a/src/editable.ts +++ b/src/draggable.ts @@ -3,7 +3,7 @@ import cloneDeep from 'lodash/cloneDeep'; import isEqual from 'lodash/isEqual'; -import { Circuit, Operation } from './circuit'; +import { Operation } from './circuit'; import { box } from './formatters/formatUtils'; import { Register } from './register'; import { Sqore } from './sqore'; @@ -26,7 +26,8 @@ interface Context { * @param sqore Sqore object * @param onCircuitChange User-provided callback function triggered when circuit is changed */ -const addEditable = (container: HTMLElement, sqore: Sqore, onCircuitChange?: (circuit: Circuit) => void): void => { +const extensionDraggable = (container: HTMLElement, sqore: Sqore, useRender: () => void): void => { + console.log('Draggable extension is running'); const svg = container.querySelector('svg') as SVGElement; const context: Context = { @@ -34,7 +35,7 @@ const addEditable = (container: HTMLElement, sqore: Sqore, onCircuitChange?: (ci svg, operations: sqore.circuit.operations, wireData: _wireData(container), - renderFn: _renderFn(container, sqore, onCircuitChange), + renderFn: useRender, paddingY: 20, selectedId: null, selectedWire: null, @@ -518,20 +519,6 @@ const _lastIndex = (dataId: string): number | undefined => { return _indexes(dataId).pop(); }; -/** - * Return a render function with the onCircuitChange callback attached to it - */ -const _renderFn = ( - container: HTMLElement, - sqore: Sqore, - onCircuitChange?: (circuit: Circuit) => void, -): (() => void) => { - return () => { - sqore.draw(container, 0, true, onCircuitChange); - if (onCircuitChange) onCircuitChange(sqore.circuit); - }; -}; - /** * Object exported for unit testing */ @@ -555,4 +542,4 @@ const exportedForTesting = { _lastIndex, }; -export { addEditable, Context, _equivOperation, exportedForTesting }; +export { extensionDraggable, Context, _equivOperation, exportedForTesting }; diff --git a/src/index.ts b/src/index.ts index fdc0c9a2..2d384b84 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,6 @@ import { Sqore } from './sqore'; import { Circuit } from './circuit'; import { StyleConfig } from './styles'; -import { addPanel } from './panel'; /** * Render `circuit` into `container` at the specified layer depth. @@ -16,18 +15,8 @@ import { addPanel } from './panel'; * @param isEditable Optional value enabling/disabling editable feature * @param onCircuitChange Optional function to trigger when changing elements in circuit */ -export const draw = ( - circuit: Circuit, - container: HTMLElement, - style: StyleConfig | string = {}, - renderDepth = 0, - isEditable?: boolean, - onCircuitChange?: (circuit: Circuit) => void, -): void => { - const sqore = new Sqore(circuit, style); - - sqore.draw(container, renderDepth, isEditable, onCircuitChange); - addPanel(container, sqore); +export const create = (circuit: Circuit, style: StyleConfig | string = {}): Sqore => { + return new Sqore(circuit, style); }; export { STYLES } from './styles'; diff --git a/src/panel.ts b/src/panel.ts index 7ffad9bd..4f08b715 100755 --- a/src/panel.ts +++ b/src/panel.ts @@ -1,23 +1,34 @@ import range from 'lodash/range'; import { Operation } from './circuit'; -import { _equivOperation } from './editable'; +import { _equivOperation } from './draggable'; +import { Register } from './register'; import { Sqore } from './sqore'; -const addPanel = (container: HTMLElement, sqore: Sqore): void => { +interface Context { + operation: Operation | undefined; +} + +const context: Context = { + operation: undefined, +}; + +const extensionPanel = (container: HTMLElement, sqore: Sqore, useRender: () => void): void => { const elems = container.querySelectorAll('[data-id]'); elems.forEach((elem) => elem.addEventListener('mousedown', () => { const dataId = elem.getAttribute('data-id'); const operation = _equivOperation(dataId, sqore.circuit.operations); - const newPanelElem = _panel(qubitSize, dispatch, operation || undefined); + context.operation = operation || undefined; + const newPanelElem = _panel(qubitSize, dispatch, context.operation); container.replaceChild(newPanelElem, panelElem); panelElem = newPanelElem; }), ); - const dispatch = reducer(container, sqore); + const dispatch = reducer(container, sqore, useRender); const qubitSize = sqore.circuit.qubits.length; - let panelElem = _panel(qubitSize, dispatch); - container.prepend(panelElem); + let panelElem = _panel(qubitSize, dispatch, context.operation); + const prevPanelElem = container.querySelector('.panel'); + prevPanelElem ? container.replaceChild(panelElem, prevPanelElem) : container.prepend(panelElem); }; interface Action { @@ -25,19 +36,27 @@ interface Action { payload: unknown; } -const reducer = (container: HTMLElement, sqore: Sqore) => (initial: Operation | undefined, action: Action) => { - if (initial == null) return; - - switch (action.type) { - case 'TARGET': { - Object.assign(initial, { ...initial, targets: action.payload }); +const reducer = + (_container: HTMLElement, _sqore: Sqore, useRender: () => void) => + (operation: Operation | undefined, action: Action) => { + if (operation == undefined) return; + + switch (action.type) { + case 'TARGET': { + operation.targets = action.payload as Register[]; + break; + } + case 'CONTROLS': { + operation.controls = action.payload as Register[]; + break; + } + case 'DISPLAY_ARGS': { + operation.displayArgs = action.payload as string; + break; + } } - case 'CONTROLS': { - Object.assign(initial, { ...initial, controls: action.payload }); - } - } - sqore.draw(container); -}; + useRender(); + }; const _panel = (qubitSize: number, dispatch: Dispatch, operation?: Operation) => { const options = range(qubitSize).map((i) => ({ value: `${i}`, text: `q${i}` })); @@ -49,7 +68,7 @@ const _panel = (qubitSize: number, dispatch: Dispatch, operation?: Operation) => _children(panelElem, [ _select('Target', 'target-input', options, target || 0, dispatch, operation), _checkboxes('Controls', 'controls-input', options, controls || [], dispatch, operation), - _text('Display', 'display-input', 'display-arg'), + _text('Display', 'display-input', dispatch, operation), ]); return panelElem; @@ -71,7 +90,7 @@ interface Option { } interface Dispatch { - (initial: Operation | undefined, action: Action): void; + (operation: Operation | undefined, action: Action): void; } const _select = ( @@ -85,7 +104,7 @@ const _select = ( const optionElems = options.map(({ value, text }) => _option(value, text)); const selectElem = _elem('select') as HTMLSelectElement; _children(selectElem, optionElems); - operation == null && selectElem.setAttribute('disabled', 'true'); + operation == undefined && selectElem.setAttribute('disabled', 'true'); selectElem.selectedIndex = selectedIndex; const labelElem = _elem('label') as HTMLLabelElement; @@ -96,7 +115,7 @@ const _select = ( _children(divElem, [labelElem, selectElem]); selectElem.onchange = () => { - dispatch(operation, { type: 'TARGET', payload: [{ qId: selectElem.value }] }); + dispatch(operation, { type: 'TARGET', payload: [{ qId: parseInt(selectElem.value) }] }); }; return divElem; @@ -121,12 +140,12 @@ const _checkboxes = ( const elem = _checkbox(option.value, option.text); const inputElem = elem.querySelector('input') as HTMLInputElement; selectedIndexes.includes(index) && inputElem.setAttribute('checked', 'true'); - operation == null && inputElem.setAttribute('disabled', 'true'); + operation == undefined && inputElem.setAttribute('disabled', 'true'); inputElem.onchange = () => { const checkedElems = Array.from(divElem.querySelectorAll('input:checked')); const newControls = checkedElems.map((elem) => ({ - qId: elem.value, + qId: parseInt(elem.value), })); dispatch(operation, { type: 'CONTROLS', payload: newControls }); }; @@ -155,13 +174,19 @@ const _checkbox = (value: string, text: string) => { return labelElem; }; -const _text = (label: string, className: string, value: string) => { +const _text = (label: string, className: string, dispatch: Dispatch, operation?: Operation) => { const labelElem = _elem('label') as HTMLLabelElement; labelElem.textContent = label; const textElem = _elem('input') as HTMLInputElement; + operation == undefined && textElem.setAttribute('disabled', 'true'); textElem.type = 'text'; - textElem.value = value; + textElem.value = operation?.displayArgs || ''; + textElem.setAttribute('autofocus', 'true'); + + textElem.onchange = () => { + dispatch(operation, { type: 'DISPLAY_ARGS', payload: textElem.value }); + }; const divElem = _elem('div'); divElem.className = className; @@ -170,4 +195,4 @@ const _text = (label: string, className: string, value: string) => { return divElem; }; -export { addPanel }; +export { extensionPanel }; diff --git a/src/sqore.ts b/src/sqore.ts index b88db601..1433e54e 100644 --- a/src/sqore.ts +++ b/src/sqore.ts @@ -3,7 +3,8 @@ import { Circuit, ConditionalRender, Operation } from './circuit'; import { svgNS } from './constants'; -import { addEditable } from './editable'; +import { extensionDraggable } from './draggable'; +import { extensionPanel } from './panel'; import { formatGates } from './formatters/gateFormatter'; import { formatInputs } from './formatters/inputFormatter'; import { formatRegisters } from './formatters/registerFormatter'; @@ -32,6 +33,10 @@ type GateRegistry = { [id: string]: Operation; }; +type Extension = { + (container: HTMLElement, sqore: Sqore, useRender: () => void): void; +}; + /** * Entrypoint class for rendering circuit visualizations. */ @@ -39,6 +44,8 @@ export class Sqore { circuit: Circuit; style: StyleConfig = {}; gateRegistry: GateRegistry = {}; + extensions: Extension[] = []; + renderDepth = 0; /** * Initializes Sqore object with custom styles. @@ -49,6 +56,7 @@ export class Sqore { constructor(circuit: Circuit, style: StyleConfig | string = {}) { this.circuit = circuit; this.style = this.getStyle(style); + this.extensions = []; } /** @@ -56,37 +64,15 @@ export class Sqore { * * @param container HTML element for rendering visualization into. * @param renderDepth Initial layer depth at which to render gates. - * @param isEditable Optional value enabling/disabling editable feature - * @param onCircuitChange Optional function to trigger when changing elements in circuit */ - draw( - container: HTMLElement, - renderDepth = 0, - isEditable?: boolean, - onCircuitChange?: (circuit: Circuit) => void, - ): void { + draw(container: HTMLElement, renderDepth = 0): Sqore { // Inject into container if (container == null) throw new Error(`Container not provided.`); - // Create copy of circuit to prevent mutation - const circuit: Circuit = JSON.parse(JSON.stringify(this.circuit)); - - // Assign unique IDs to each operation - circuit.operations.forEach((op, i) => this.fillGateRegistry(op, i.toString())); - - // Render operations at starting at given depth - circuit.operations = this.selectOpsAtDepth(circuit.operations, renderDepth); + this.renderDepth = renderDepth; + this.renderCircuit(container); - // If only one top-level operation, expand automatically: - if ( - circuit.operations.length == 1 && - circuit.operations[0].dataAttributes != null && - circuit.operations[0].dataAttributes.hasOwnProperty('id') - ) { - const id: string = circuit.operations[0].dataAttributes['id']; - this.expandOperation(circuit.operations, id); - } - this.renderCircuit(container, circuit, isEditable, onCircuitChange); + return this; } /** @@ -116,12 +102,27 @@ export class Sqore { * @param isEditable Optional value enabling/disabling editable feature * @param onCircuitChange Optional function to trigger when changing elements in circuit */ - private renderCircuit( - container: HTMLElement, - circuit: Circuit, - isEditable?: boolean, - onCircuitChange?: (circuit: Circuit) => void, - ): void { + private renderCircuit(container: HTMLElement): void { + // Create copy of circuit to prevent mutation + const circuit: Circuit = JSON.parse(JSON.stringify(this.circuit)); + const renderDepth = this.renderDepth; + + // Assign unique IDs to each operation + circuit.operations.forEach((op, i) => this.fillGateRegistry(op, i.toString())); + + // Render operations at starting at given depth + circuit.operations = this.selectOpsAtDepth(circuit.operations, renderDepth); + + // If only one top-level operation, expand automatically: + if ( + circuit.operations.length == 1 && + circuit.operations[0].dataAttributes != null && + circuit.operations[0].dataAttributes.hasOwnProperty('id') + ) { + const id: string = circuit.operations[0].dataAttributes['id']; + this.expandOperation(circuit.operations, id); + } + // Create visualization components const composedSqore: ComposedSqore = this.compose(circuit); const svg: SVGElement = this.generateSvg(composedSqore); @@ -129,9 +130,12 @@ export class Sqore { previousSvg == null // ? container.appendChild(svg) : container.replaceChild(svg, previousSvg); - this.addGateClickHandlers(container, circuit, isEditable, onCircuitChange); + this.addGateClickHandlers(container, circuit); - isEditable && addEditable(container, this, onCircuitChange); + // Run extensions after every render or re-render + const extensions = this.extensions; + extensions != null && + extensions.map((extension) => extension(container, this, () => this.renderCircuit(container))); } /** @@ -251,14 +255,9 @@ export class Sqore { * @param onCircuitChange Optional function to trigger when changing elements in circuit * */ - private addGateClickHandlers( - container: HTMLElement, - circuit: Circuit, - isEditable?: boolean, - onCircuitChange?: (circuit: Circuit) => void, - ): void { + private addGateClickHandlers(container: HTMLElement, circuit: Circuit): void { this.addClassicalControlHandlers(container); - this.addZoomHandlers(container, circuit, isEditable, onCircuitChange); + this.addZoomHandlers(container, circuit); } /** @@ -318,12 +317,7 @@ export class Sqore { * @param onCircuitChange Optional function to trigger when changing elements in circuit * */ - private addZoomHandlers( - container: HTMLElement, - circuit: Circuit, - isEditable?: boolean, - onCircuitChange?: (circuit: Circuit) => void, - ): void { + private addZoomHandlers(container: HTMLElement, circuit: Circuit): void { container.querySelectorAll('.gate .gate-control').forEach((ctrl) => { // Zoom in on clicked gate ctrl.addEventListener('click', (ev: Event) => { @@ -334,7 +328,7 @@ export class Sqore { } else if (ctrl.classList.contains('gate-expand')) { this.expandOperation(circuit.operations, gateId); } - this.renderCircuit(container, circuit, isEditable, onCircuitChange); + this.renderCircuit(container); ev.stopPropagation(); } }); @@ -379,4 +373,24 @@ export class Sqore { } }); } + + public useDraggable(): Sqore { + this.extensions.push(extensionDraggable); + return this; + } + + public usePanel(): Sqore { + this.extensions.push(extensionPanel); + return this; + } + + public useOnCircuitChange(callback: (circuit: Circuit) => void): Sqore { + const extensionOnCircuitChange = ( + _container: HTMLElement, // + _sqore: Sqore, + _useRender: () => void, + ) => callback(this.circuit); + this.extensions.push(extensionOnCircuitChange); + return this; + } } From 2a5c35efc8ce9ec88010b9322786ef118e86ad58 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen <37777232+aaronthangnguyen@users.noreply.github.com> Date: Wed, 13 Jul 2022 15:20:41 -0400 Subject: [PATCH 061/108] Sync with `main` (#77) * v 1.0.5 * Fixing jupyter viewer display (#70) Co-authored-by: Andres Paz From 473914ad69e7d331e6e18d50c33b71806adbdde6 Mon Sep 17 00:00:00 2001 From: Aaron Nguyen Date: Wed, 13 Jul 2022 17:22:06 -0400 Subject: [PATCH 062/108] Add styles --- example/index.html | 1 + src/draggable.ts | 13 +------------ src/panel.ts | 14 +++++++++++++- src/sqore.ts | 26 ++++++++++++-------------- src/styles.ts | 27 +++++++++++++++++++++++++-- 5 files changed, 52 insertions(+), 29 deletions(-) diff --git a/example/index.html b/example/index.html index e612d7b8..196a80c5 100644 --- a/example/index.html +++ b/example/index.html @@ -11,6 +11,7 @@