1// Copyright 2021 The Cirq Developers
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {
16  Scene,
17  PerspectiveCamera,
18  WebGLRenderer,
19  OrthographicCamera,
20  Vector3,
21  Box3,
22} from 'three';
23import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls';
24import {GridCircuit} from './grid_circuit';
25import {SymbolInformation} from './components/types';
26
27class CircuitScene extends Scene {
28  private WIDTH = 1000;
29  private HEIGHT = 700;
30
31  public camera: PerspectiveCamera | OrthographicCamera;
32  public renderer: WebGLRenderer;
33  public controls: OrbitControls;
34
35  /**
36   * Support for both the Perspective and Orthographic
37   * cameras offered by three.js
38   */
39  private perspectiveCamera: PerspectiveCamera;
40  private orthographicCamera: OrthographicCamera;
41  private perspectiveControls: OrbitControls;
42  private orthographicControls: OrbitControls;
43
44  /**
45   * Class constructor.
46   * Creates a three.js scene object, adds it to the container element
47   * at the designated id, and adds the orbit control functionality.
48   * @param sceneId The container id that will host the three.js scene object
49   * @returns The three.js scene object
50   */
51  constructor(sceneId: string) {
52    super();
53
54    this.renderer = new WebGLRenderer({alpha: true, antialias: true});
55    this.renderer.setSize(this.WIDTH, this.HEIGHT);
56
57    this.perspectiveCamera = new PerspectiveCamera(
58      75,
59      this.WIDTH / this.HEIGHT,
60      0.1,
61      1000
62    );
63
64    this.orthographicCamera = new OrthographicCamera(
65      this.WIDTH / this.HEIGHT / -2,
66      this.WIDTH / this.HEIGHT / 2,
67      this.HEIGHT / this.WIDTH / 2,
68      this.HEIGHT / this.WIDTH / -2,
69      0.1,
70      100
71    );
72    this.orthographicCamera.zoom = 0.1;
73    // The default camera is the Perspective camera
74    this.camera = this.perspectiveCamera;
75
76    this.perspectiveControls = new OrbitControls(
77      this.perspectiveCamera,
78      this.renderer.domElement
79    );
80    this.orthographicControls = new OrbitControls(
81      this.orthographicCamera,
82      this.renderer.domElement
83    );
84    // The default controls are the Orbit controls for the Perspective camera
85    this.controls = this.perspectiveControls;
86
87    const el = document.getElementById(sceneId)!;
88    el.appendChild(this.renderer.domElement);
89
90    this.animate();
91  }
92
93  /**
94   * Orients the camera and controls based off of the generated circuit,
95   * setting its position, zoom, and orientation.
96   * @param circuit The circuit that the camera is oriented off of.
97   */
98  setCameraAndControls(circuit: GridCircuit) {
99    // Set the camera positions
100    this.fitPerspectiveCamera(circuit);
101    this.fitOrthographicCamera(circuit);
102
103    // Tells the camera which way is up.
104    this.camera.up.set(0, 1, 0);
105
106    // The camera will always look at circuit to start
107    this.camera.lookAt(circuit.position);
108
109    // Tells the control anchor to be set to the center
110    // of the circuit.
111    const center = new Box3().setFromObject(circuit).getCenter(new Vector3());
112    this.controls.target.set(center.x, center.y, center.z);
113  }
114
115  /**
116   * Toggles between a Perspective and Orthographic camera, resetting
117   * the camera to the default orientation each time.
118   * @param circuit The circuit that the camera is oriented off of.
119   */
120  toggleCamera(circuit: GridCircuit) {
121    if (this.camera instanceof PerspectiveCamera) {
122      this.camera = this.orthographicCamera;
123      this.controls = this.orthographicControls;
124    } else {
125      this.camera = this.perspectiveCamera;
126      this.controls = this.perspectiveControls;
127    }
128
129    this.setCameraAndControls(circuit);
130  }
131
132  private animate() {
133    requestAnimationFrame(this.animate.bind(this));
134    this.controls.update();
135    this.renderer.render(this, this.camera);
136  }
137
138  private fitPerspectiveCamera(circuit: GridCircuit) {
139    const boundingBox = new Box3();
140    boundingBox.setFromObject(circuit);
141
142    const size = boundingBox.getSize(new Vector3());
143
144    const max = Math.max(size.x, size.y, size.z);
145    const fov = this.perspectiveCamera.fov * (Math.PI / 180);
146
147    const z = Math.abs(max / Math.sin(fov / 2));
148
149    this.perspectiveCamera.position.x = 0;
150    this.perspectiveCamera.position.y = 2.5;
151    this.perspectiveCamera.position.z = z;
152  }
153
154  private fitOrthographicCamera(circuit: GridCircuit) {
155    // TODO: Issue #4395. For more precision, calculate the bounding box of the
156    // circuit and set the orthographic camera to the four corners
157    // plus an offset.
158    this.orthographicCamera.position.x = 1;
159    this.orthographicCamera.position.y = 2;
160    this.orthographicCamera.zoom = 0.07;
161    this.orthographicCamera.updateProjectionMatrix();
162
163    this.orthographicCamera.lookAt(circuit.position);
164  }
165}
166
167/**
168 * Creates and returns an empty GridCircuit object with qubits at the
169 * designated coordinates. The returned GridCircuit object can then take
170 * input to add gates to the circuit.
171 * @param qubits A list of GridCoord objects representing the location of
172 * each qubit.
173 * @param numMoments The number of total moments in the circuit
174 * @param sceneId The container id with the three.js scene that will render
175 * the three.js components
176 * @param padding_factor A number that represents how much the visualization
177 * should be scaled on the x,z coordinate plane. Default is 1.
178 * @returns A GridCircuit object
179 */
180export function createGridCircuit(
181  symbol_info: SymbolInformation[],
182  numMoments: number,
183  sceneId: string,
184  padding_factor = 1
185): {circuit: GridCircuit; scene: CircuitScene} {
186  const scene = new CircuitScene(sceneId);
187
188  const circuit = new GridCircuit(numMoments, symbol_info, padding_factor);
189  scene.add(circuit);
190  scene.setCameraAndControls(circuit);
191
192  return {circuit, scene};
193}
194