Repository: googlecreativelab/webvr-musicalforest Branch: master Commit: 99d6007715d2 Files: 176 Total size: 1.0 MB Directory structure: gitextract_yh_t2yfb/ ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app.yaml ├── backend/ │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── package.json │ └── src/ │ ├── config.js │ ├── datastore/ │ │ ├── datastore-constants.js │ │ └── index.js │ ├── index.js │ ├── logger/ │ │ ├── cloud-logger.js │ │ └── index.js │ ├── messages/ │ │ ├── index.js │ │ ├── message-actions.js │ │ ├── message-constants.js │ │ ├── message-handler.js │ │ ├── message-rate-limiter.js │ │ ├── message-sagas.js │ │ ├── message-schema.js │ │ └── message-validator.js │ ├── pubsub/ │ │ ├── index.js │ │ └── pubsub-client.js │ ├── rooms/ │ │ ├── index.js │ │ ├── room-data-actions.js │ │ ├── room-data-constants.js │ │ ├── room-data-reducer.js │ │ ├── room-names.js │ │ ├── room-sagas.js │ │ ├── room-state-actions.js │ │ ├── room-state-constants.js │ │ ├── room-state-machine.js │ │ ├── room-state-reducer.js │ │ └── room-state-utils.js │ ├── s11n.js │ ├── server/ │ │ ├── app-server.js │ │ ├── balancer.js │ │ ├── index.js │ │ ├── server-actions.js │ │ ├── server-constants.js │ │ ├── server-reducer.js │ │ ├── server-sagas.js │ │ ├── server-setup.js │ │ ├── server-startup.js │ │ ├── server-sync-handlers.js │ │ ├── server-sync.js │ │ ├── target-server-utils.js │ │ └── websocket-server.js │ ├── spheres/ │ │ ├── index.js │ │ ├── sphere-constants.js │ │ └── trees/ │ │ ├── 0.js │ │ ├── 1.js │ │ ├── 2.js │ │ ├── 3.js │ │ ├── 4.js │ │ └── 5.js │ ├── store.js │ └── utils/ │ ├── index.js │ ├── reducer-utils.js │ ├── saga-utils.js │ ├── string-utils.js │ ├── url-utils.js │ └── websocket-utils.js ├── config.js ├── config.template ├── index.html ├── js/ │ ├── ascene.js │ ├── components/ │ │ ├── background-objects.js │ │ ├── ball.js │ │ ├── bg-tree-ring-material.js │ │ ├── clicker.js │ │ ├── controller-material.js │ │ ├── controllers.js │ │ ├── copresence-server-messages.js │ │ ├── copresence-server.js │ │ ├── daydream-manager.js │ │ ├── daydream-pointer.js │ │ ├── fake-light.js │ │ ├── ga.js │ │ ├── gaze.js │ │ ├── grab-move.js │ │ ├── haptics.js │ │ ├── headset-material.js │ │ ├── listener.js │ │ ├── palette.js │ │ ├── proximity-check.js │ │ ├── quaternion.js │ │ ├── smooth-motion.js │ │ ├── teleport.js │ │ ├── tone.js │ │ ├── tool-tips.js │ │ ├── touch-color.js │ │ ├── tree.js │ │ └── wasd-boundaries.js │ ├── core/ │ │ ├── color-set.js │ │ ├── colors.js │ │ ├── instrument.js │ │ ├── instruments.js │ │ ├── shape-data.js │ │ └── shapes.js │ ├── index.js │ ├── notes/ │ │ ├── note-head-cube.js │ │ ├── note-head-sphere.js │ │ ├── note-head-tetra.js │ │ ├── note-head.js │ │ ├── note-shadow-cube.js │ │ ├── note-shadow-sphere.js │ │ ├── note-shadow-tetra.js │ │ ├── note-shadow.js │ │ ├── note-stem.js │ │ └── note.js │ ├── orientation-arm-model.js │ ├── shaders/ │ │ ├── ball-shader.js │ │ ├── bg-tree-shader.js │ │ ├── circle-shader.js │ │ └── shader-chunks.js │ ├── splash.js │ ├── util/ │ │ ├── browserCheck.js │ │ ├── random-range-1d.js │ │ ├── random-range-3d.js │ │ ├── tetrahedron.js │ │ └── trace.js │ └── util.js ├── package.json ├── python/ │ ├── __init__.py │ ├── base/ │ │ ├── __init__.py │ │ ├── api_fixer.py │ │ ├── api_fixer_test.py │ │ ├── constants.py │ │ ├── handlers.py │ │ ├── handlers_test.py │ │ ├── models.py │ │ ├── models_test.py │ │ ├── xsrf.py │ │ └── xsrf_test.py │ ├── country_servers.py │ ├── handlers.py │ ├── main.py │ └── main_test.py ├── static/ │ ├── audio/ │ │ ├── bg.ogg │ │ ├── marimba/ │ │ │ ├── 0.ogg │ │ │ ├── 1.ogg │ │ │ ├── 2.ogg │ │ │ ├── 3.ogg │ │ │ ├── 4.ogg │ │ │ └── 5.ogg │ │ ├── percussion/ │ │ │ ├── 0.ogg │ │ │ ├── 1.ogg │ │ │ ├── 2.ogg │ │ │ ├── 3.ogg │ │ │ ├── 4.ogg │ │ │ └── 5.ogg │ │ └── voice/ │ │ ├── 0.ogg │ │ ├── 1.ogg │ │ ├── 2.ogg │ │ ├── 3.ogg │ │ ├── 4.ogg │ │ └── 5.ogg │ └── models/ │ ├── bg-tree-1.obj │ ├── bg-tree-2.obj │ ├── bg-tree-3.obj │ ├── bg-tree-ring.obj │ ├── controller.dae │ ├── door-frame.obj │ ├── door-glow.obj │ ├── door.dae │ ├── headset.dae │ ├── stump-shadow.obj │ ├── stump.obj │ └── target.obj ├── style/ │ └── splash.scss └── third_party/ └── aframe-daydream-controller-component/ ├── LICENSE ├── METADATA └── daydream-controller.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /node_modules /.idea .DS_Store build/main.js *.pyc build/ *.swp .sass-cache npm-debug.log *.asd ================================================ FILE: CONTRIBUTING.md ================================================ # How to contribute We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution, this simply gives us permission to use and redistribute your contributions as part of the project. Head over to to see your current agreements on file or to sign a new one. You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. ## Code reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help] for more information on using pull requests. [GitHub Help]: https://help.github.com/articles/about-pull-requests/ ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================
This project is no longer actively maintained by the Google Creative Lab but remains here in a read-only Archive mode so that it can continue to assist developers that may find the examples helpful. We aren’t able to address all pull requests or bug reports but outstanding issues will remain in read-only mode for reference purposes. Also, please note that some of the dependencies may not be up to date and there hasn’t been any QA done in a while so your mileage may vary.

For more details on how Archiving affects Github repositories see this documentation .

We welcome users to fork this repository should there be more useful, community-driven efforts that can help continue what this project began.
# Musical Forest Musical Forest is a multiplayer musical experiment in virtual reality. It uses copresence —the synchronization of multiple people in a virtual space— to allow people to play music together in VR. WebSockets are used to sync all of the connected players in real-time. Musical Forest, a [WebVR Experiment](https://webvrexperiments.com/).

## Basic Interaction Each of the shapes triggers a note when it’s hit. Users can navigate the space, play notes and also hear and play with all of the other players in the space at the same time.
![alt text](https://forest.webvrexperiments.com/static/img/MusicalForest.gif "The Musical Forest, mixed reality interaction example")

## Objects & Audio There are three different shapes of musical objects corresponding to three different sets of sounds. Each set has six notes, chosen from a pentatonic scale. * Spheres: Percussion (Conga, Woodblock, COWBELL!) * Triangular Pyramids: Voice + Flute * Cubes: Marimba Sounds are positioned in 3D space using the Web Audio API’s PannerNode.

## Headsets & Interaction Models Musical Forest responsively adapts features depending on the capabilities of the VR Hardware. ### Vive/Oculus Play: hit the shapes with your controller to hear its sound. The volume of the sound changes depending on how hard you hit it.
Create: tap the trigger to create a new shape. Rotate the circular pad to change the note.
Rearrange: place the controller over an existing object, press and hold the trigger to grab it. Move your controller and release to move it to a new space. Hovering your controller over an object and rotating the circular pad will change the type of shape and it’s sound.
Navigation: move within the bounds of your roomscale environment to interact with the objects within the experience. ### Daydream Play: Hit the shapes with your controller to hear their sounds.
Navigation: point the Daydream controller at the ground and a circle will appear. Press the main button on the controller to teleport to that highlighted spot. ### Cardboard Play: gaze at an object and see it glow. Tap the interaction button to hear that object.
Navigation: gaze at the ground and a circle will appear. Tap the interaction button to teleport to the highlighted spot. ### Magic Window Interaction: tap any object to hear the sound of that object.
Navigation: gaze at the ground and a circle will appear. Tap anywhere to teleport to where the reticle is pointing. ### Desktop Interaction: use the mouse to click any object to hear its sound. The volume of the sound is dictated by the object’s distance from the user.
Navigation: use the WASD keys on the keyboard. Use the mouse to change the field of view by clicking in empty space and dragging.

## Technologies Used ### Frontend Musical Forest uses [A-Frame](https://aframe.io) which is built with the WebVR standard and [Tone.js](https://github.com/Tonejs/Tone.js/) for sound. ### Backend The backend is developed in Node.js. To get a full overview of the technologies and libraries used, see the [backend readme](backend/README.md#Description)

## Running the Frontend Code Download the source code, and install all dependencies by running `npm install`. To run the frontend, run `npm start`, this will start a local webserver using `budo` connecting to the default backend server. If you have a local backend server running, append `?server=localhost` to the url.

## Acknowledgements [Manny Tan](https://github.com/mannytan), [Igor Clark](https://github.com/igorclark), [Yotam Mann](https://github.com/tambien), [Alexander Chen](https://github.com/alexanderchen), [Jonas Jongejan](https://github.com/halfdanj), [Jeremy Abel](https://github.com/jeremyabel), [Saad Moosajee](https://github.com/moosajee), Alex Jacobo-Blonder, [Ryan Burke](https://github.com/ryburke), and many others at Google Creative Lab. ================================================ FILE: app.yaml ================================================ # Copyright 2017 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ########################################################################### # DO NOT MODIFY THIS FILE WITHOUT UNDERSTANDING THE SECURITY IMPLICATIONS # ########################################################################### # The version is automatically generated based on the current git hash. # If there are uncommitted changes, a '-dev' suffix will be added. You do # not need to modify it here. runtime: python27 api_version: 1 threadsafe: true handlers: # - url: / # static_files: "index.html" # secure: always # upload: index\.html # http_headers: # X-Frame-Options: "DENY" # Strict-Transport-Security: "max-age=2592000; includeSubdomains" # X-Content-Type-Options: "nosniff" # X-XSS-Protection: "1; mode=block" - url: /build/ static_dir: build/ secure: always http_headers: X-Frame-Options: "DENY" Strict-Transport-Security: "max-age=2592000; includeSubdomains" X-Content-Type-Options: "nosniff" X-XSS-Protection: "1; mode=block" - url: /static/ static_dir: static/ secure: always http_headers: X-Frame-Options: "DENY" Strict-Transport-Security: "max-age=2592000; includeSubdomains" X-Content-Type-Options: "nosniff" X-XSS-Protection: "1; mode=block" # All URLs should be mapped via the *_ROUTES variables in the src/main.py file. # See https://webapp-improved.appspot.com/guide/routing.html for information on # how URLs are routed in the webapp2 framework. Do not add additional handlers # directly here. - url: /.* script: python.main.app secure: always libraries: - name: django version: latest - name: jinja2 version: latest - name: webapp2 version: latest skip_files: - ^(.*/)?#.*#$ - ^(.*/)?.*~$ - ^(.*/)?.*\.py[co]$ - ^(.*/)?.*/RCS/.*$ - ^(.*/)?\..*$ - README - util.sh - run_tests.py - .*_test.py - js/.* - backend/.* - ^node_modules/(.*/)? - ^(.*/)?app\.yaml - ^(.*/)?app\.yml - ^\.git/.* ================================================ FILE: backend/.babelrc ================================================ { "presets": ["es2015", "stage-2"], "plugins": [] } ================================================ FILE: backend/.gitignore ================================================ # Logs logs *.log npm-debug.log* # tmp/testing scratch tmp *.bak # vim swap files *.swp *.swo # Mac filesystem entries .DS_Store # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # babel-translated es5 output dist # Dependency directories node_modules jspm_packages # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # vagrant anything .vagrant # allow .gitignore files !.gitignore ================================================ FILE: backend/README.md ================================================ # webvr-experiments: Musical Forest back-end ## Contents * [Description](#description) * [Running the app locally](#running-the-app-locally) * [Deploying in production](#deploying-in-production) * [How the app works](#how-the-app-works) * [Basic operation](#basic-operation) * [Managing state](#managing-state) * [Starting the server](#starting-the-server) * [Handling SSL connections](#handling-ssl-connections) * [Managing connections](#managing-connections) * [Managing peer servers](#managing-peer-servers) * [Managing rooms](#managing-rooms) * [Handling client messages](#handling-client-messages) * [Connecting clients to the server](#connecting-clients-to-the-server) * [Messages](#messages) * [URLs](#urls) * [Interacting in rooms](#interacting-in-rooms) --- ## Description This is the backend server for the Chrome WebVR Experiment [Musical Forest](https://forest.webvrexperiments.com/). It's a dynamic room-based [WebSocket](https://en.wikipedia.org/wiki/WebSocket) server app using Google Cloud [PubSub](https://cloud.google.com/pubsub/) and [Datastore](https://cloud.google.com/datastore/). It's written in Javascript ES6 using [`node.js`](https://nodejs.org/) and the following [`npm`](https://www.npmjs.com/ )modules: * `uws` websocket server * `redux` and `javascript-state-machine` for state management * `redux-sagas` for managing asynchronous/long-running tasks and composite async actions * `@google-cloud/*` for using GCP services * `http-proxy` and `hashring` to distribute connections consistently between app instances * `fast-ratelimit` to hard-limit per-client message rates * `tracer` for logging --- ## Running the app locally #### Install prerequisites * [Install gcloud](https://cloud.google.com/sdk/downloads) * `node.js` LTS version 6 + `npm` (Mac: `brew install node@6 npm` - [notes about installing `node` on Mac with Homebrew](http://apple.stackexchange.com/questions/171530/how-do-i-downgrade-node-or-install-a-specific-previous-version-using-homebrew/207883)) * `gcc` C compiler for native `node.js` modules (Xcode 8.1+ if running on Mac) * [`jq`](https://stedolan.github.io/jq/) (Mac: `brew install jq`) #### Authenticate with GCP ``` gcloud auth login gcloud config set project gcloud auth application-default login ``` #### Install node.js modules `npm install --no-optional` #### Run the app Start the `datastore` & `pubsub` emulators: `npm run emulators` In a new shell, start the app in dev mode using the emulators: `npm run start_emulated` You can now connect to the server from the main web app by appending `?server=ws://localhost:9100` to the URL. #### Test a connection to the app from command line (optional) Install a solid command-line websocket client: * Install [go](https://golang.org/) (Mac: `brew install go # make sure to set up a ${GOPATH} dir & env var`) * `go get -u github.com/hashrocket/ws` Connect to a websocket: * `ws ws://localhost:9100/` (see [URLs](#urls) below regarding viewer types) * _paste in a message, e.g. `{"t":"g_s","d":{"spId":"973c742d-866f-4b25-9ae6-8830e7f13e59"}}` as above_ #### Run multiple local app instances (optional) The app defaults to using ports `8100` for the application server and `9100` for the inter-node connection balancer (see `src/server/server-startup.js` and `src/server/balancer.js`). To run multiple instances, `export WS_SERVER_PORT=` and `export WS_BALANCER_PORT=` in a new shell before running `npm run start_emulated`. Instances will coordinate via the local PubSub and Datastore emulators. --- ## Deploying in production The app is designed to be deployed as a series of single-threaded application instances (pods) running in a Docker container cluster via [Kubernetes](https://kubernetes.io/) on [Google Container Engine](https://cloud.google.com/container-engine/). Deploying and running clustered applications is beyond the scope of this document, but the following configuration and setup points are necessary to run this app in such environments. You'll need to create a `Dockerfile` for your environment along with an `entrypoint` script to run the application on the pods, and modify the application configuration as follows: 1. Your `entrypoint` script must `export` the following environment variables: * `PROJECT_ID` - your Google Cloud Platform project name * `ENVIRONMENT_NAME` - a unique identifier to separate different deployment environments, and keeps Datastore and PubSub resources separate for each deployment within a project. As an example, running the app locally via `package.json` uses your computer's `hostname`; you might want to name a particular deployment by region or type * `LOCAL_IP_ADDRESS` - the private-range IP address by which each pod is accessible and identifies itself to other nodes in the cluster, used by pods to health-check each other when necessary 2. When automatically scaled up or down, application nodes synchronize with new and removed nodes using a PubSub topic. The name of this topic is constructed from your `ENVIRONMENT_NAME` at startup in `src/config.js` 3. Set `PRODUCTION_ENVIRONMENT_PROJECT_ID` and `PRODUCTION_ENVIRONMENT_REQUIRED_ORIGIN` in `src/config.js` to reflect the Google Cloud Platform project name and [WebSocket Origin header](https://en.wikipedia.org/wiki/WebSocket#Protocol_handshake) required for connections to your production cluster Refer to the [Container Engine](https://cloud.google.com/container-engine/docs/) and [Kubernetes](https://kubernetes.io/docs/home/) documentation for further information on how to set up and manage clusters, deployments, services, etc. --- ## How the app works ##### Basic operation The app accepts client connections to its websocket server (`src/server/websocket-server.js`), parses the requested URL to work out headset type and whether a specific room has been chosen, and then tries to connect the client to a room. It maintains state about which clients and spheres are in which room, which rooms are full or available, and handles all communications from and to clients in all rooms. ##### Managing state The app holds state in a `redux` state store (`src/store.js`), mutated in reducers (`src/*/*-reducer.js`) via messages `dispatch()`'d to the store, and accessed from the store in async or composite action sequences (`src/server/server-sagas.js`, `src/rooms/room-sagas.js`, `src/messages/message-sagas.js`) using methods provided by the `redux-sagas` module. ##### Starting the server Servers are started (`src/server/server-startup.js`) via a series of composite actions (`src/server/server-sagas.js`, `src/server/server-setup.js`) defined as `redux-sagas` generator functions, initiated by a server setup action dispatched to the `redux` store at startup (`src/index.js`). The server requires the environment variables `ENVIRONMENT_NAME`, `LOCAL_IP_ADDRESS` and `PROJECT_ID` in order to run. These are set by the `npm` and `gke` startup tasks, according to where the app's being run. ##### Handling SSL connections If the environment variable `USE_SSL` is set to `true`, the server requires the environment variable `SSL_CERT_HOST_NAME` to be set too, and retrieves the SSL certificate for that hostname on startup from a [Google Cloud Storage](https://cloud.google.com/storage/) (GCS) bucket in the GCP project, named `-ssl`. The full certificate chain file (named `fullchain.pem`) and the private key file (`privkey.pem`) must be stored in a sub-folder of that GCS bucket with the same name as the required hostname. ##### Managing connections All connections to a given room are handled by the same instance of the server. The app manages this by running a public/client-facing 'load-balancer' (`src/server/balancer.js`) which proxies client connections either to a local or peer websocket app server, according to which server is associated with the chosen room name in a persistent hash-ring. (See the `hashring` [docs](https://github.com/3rd-Eden/node-hashring) for implementation, and a helpful [explanation](http://blog.plasmaconduit.com/consistent-hashing/).) The hash-ring is stored in the server state (`src/server/server-reducer.js`). When a connection arrives, the server takes the specified room name or chooses one dynamically according to which rooms are available locally (using `avails` in `src/rooms/room-data-reducer.js` and the state machine in `src/rooms/room-state-machine.js'). It then looks up which server should handle the connection in the hash-ring and proxies the connection to that server. References to all websocket connections held by a given server are kept in the server state (`src/server/server-reducer.js`). The only non-store-managed state consists of properties (`id` and current-room info) set directly on websocket connection objects by the websocket server. ##### Managing peer servers In order to maintain a list of peers during automatic scaling of the app in production, the server connects to a PubSub 'sync channel' on startup (`src/server/server-setup.js`), and listens out for sync 'heartbeat' messages from other servers on the channel. It maintains a list of which servers are connected, adding newly-arrived servers to the hash-ring and removing them from it when they time out (stop sending sync heartbeats for a given period). Each server instance periodically writes information about its state and its PubSub subscription to a ServerSubscriptionInfo record in the Datastore (`src/server/server-sagas.js`), allowing the administrator to retrieve aggregate status info, and enabling the app to check up on timed-out or otherwise dead app instances, removing their status info & PubSub subscriptions as appropriate (also in `src/server/server-sagas.js`). ##### Managing rooms The server manages room status info (available, full, setting up, etc) using a state machine (`src/rooms/room-state-machine.js`) for each room, initialised at app startup and accessed via the `redux` store. A series of long-running tasks (`src/rooms/room-sagas.js`) handles activity on the rooms (initialising room content, starting/stopping heartbeats, client activity and sphere hold timeouts) using generator functions managed using the `redux-sagas` module. ##### Handling client messages Clients are restricted to a fixed set of messages which are specified in a JSON schema (`src/messages/message-schema.js`). The websocket server passes received client messages to a message-handler module (`src/messages/message-handler.js`). The message-handler validates incoming messages against the schema (`src/messages/message-validator.js`) and dispatches valid messages to the `redux` state store, using messages constructed from the schema (`src/messages/message-actions.js`). Invalid messages are dropped before getting to dispatch, so any message that gets through at least passes validation. A series of long-running tasks implemented as `redux-sagas` (`src/messages/message-sagas.js`) handle the validated messages and carry out the requested actions, i.e. sending client position updates and interacting with spheres. * All possible message interactions between clients and servers are specified in the JSON schema and carried out in the "sagas" - any messages not specified like so are silently dropped. * Any superfluous information in messages received from clients is silently dropped. * The websocket server rate-limits client messages on both a per-message-type and per-client basis. Default rates are specified in `RATE_LIMIT_INFO` constants (`src/config.js`), and overridden by values retrieved from `PerEnvironmentRateLimitInfo` Datastore records on startup if present (`src/server/server-sagas.js`). You can request the app reload the limit values from Datastore by sending a `SYNC_MESSAGES.LOAD_RATE_LIMITER_INFO` message to the app via PubSub. (That's a deploy/admin task not implemented in the app source, but the mechanism is visible in `sendSyncMessageOfType()` in `src/server/server-sync.js`.) Messages above the thresholds are silently dropped. * Any messages received from clients with a 'viewer' headset-type are silently dropped. --- ## Connecting clients to the server The app accepts standard WebSocket connections to "rooms" in the forest. The server receives messages over the WebSocket, sends responses and broadcasts messages from other occupants of the room all over the same connection. ---- ## Messages Here's an example of the message format: ``` { "f": "81270b00-2dde-4c33-ab4d-6cfc76b46c0c", // 'from' id of client or server sending the message "m": { // message "t": "", "d": "" } } ``` Message labels (e.g. `f` or `m` above) are set as constants in the file `src/messages/message-constants.js`. In front-end and back-end code, messages are always constructed and consumed using the labels as property names rather than fixed properties, so that the values used can be changed to friendly names during development if required, and restricted to short strings for use in production. --- ## URLs Clients make connections via URLs of the format `ws://://`. `` can be `3dof`, `6dof` or `viewer`. Any action messages sent by `viewer` connections are dropped by the server when running in production. * To join a specific room, supply both the `` and `` URL components. * To have a room chosen for you, supply only the `` component. Any other URLs are invalid and connections to them will be closed immediately with an `INVALID_URL` (`i_u`) message: ``` { "f": "81270b00-2dde-4c33-ab4d-6cfc76b46c0c", "m": { "t": "i_u" // invalid_url } } ``` --- ## Interacting in rooms * All interactions with the server are carried out using messages in the above format over the websocket connection. * Clients don't need to identify themselves with `` fields, as IDs are associated directly with client websocket connections on the server. * The server validates messages received from clients against the appropriate JSON schema for their ``, and immediately drops anything non-conforming, according to the schema in `src/messages/message-schema.js`. It also removes any superfluous content/properties in the messages before storing anything to the room state or broadcasting updates to other clients in the room. ### Joining a room When the first client joins a room, the server kicks off a heartbeat, and sends an incrementing `ROOM_HEARTBEAT` (`r_h`) sync message to all clients present in the room, every 5 seconds: ``` { "f": "a738cbe0-4dac-4389-aa20-1e6a86c166a9", "m": { "t": "r_h", "d": { "c": 3, // heartbeat count since room start "s": 15 // heartbeat seconds since room start } } } ``` When a new client joins a room, it gets sent a `CONNECTION_INFO` (`c_i`) message containing its ID and the ID of the server it's connected to: ``` { "f": "a738cbe0-4dac-4389-aa20-1e6a86c166a9", "m": { "t": "c_i", "d": { "cId": "cb439961-3b57-4403-bcc5-208b47565dd1", // clientId "srId": "a738cbe0-4dac-4389-aa20-1e6a86c166a9" // serverId } } } ``` ... followed immediately by a `ROOM_STATUS_INFO` (`r_s_i`) message containing the `ROOM_NAME` (`r_n`), a list of other occupants of the room grouped by viewer type and keyed by their IDs, and a list of spheres in the room with their tones & positions: ``` { "f": "a738cbe0-4dac-4389-aa20-1e6a86c166a9", "m": { "t": "r_s_i", "d": { "rn": "ctyw", // room name "sb": 1, // soundbank "c": { // clients, grouped by type "3dof": [ "cb439961-3b57-4403-bcc5-208b47565dd1" ] }, "s": { // spheres "6567cbaa-e6c5-457f-b338-2949b13ff17a": { "t": 4, // tone "p": { // position "x": -0.5858704723117225, "y": 1.4647926895710155, "z": 0.2328255217809887 } }, "0e19b0dd-a90b-4fc8-93d5-24a96365ce2c": { "t": 6, "p": { "x": -0.07124214717156585, "y": 0.9642806694401945, "z": 1.181824016202171 } } } } } } ``` Existing clients get a `ROOM_CLIENT_JOIN` (`r_c_j`) message indicating a new client has joined along with its id: ``` { "f": "a738cbe0-4dac-4389-aa20-1e6a86c166a9", "m": { "t": "r_c_j", "d": { "cId": "6211a260-d86e-4db9-be00-bd2eba1c2794", // clientId "ht": "3dof" // headsetType } } } ``` If the server is full, overloaded or in the process of scaling, the server sends a `BUSY_TRY_AGAIN` (`b_t_a`) message, in which case the client should just try to connect again: ``` { "f": "d02922d2-6db0-4ad1-93e1-0d778250461e", "m": { "t": "b_t_a", "d": {} } } ``` ### Leaving a room Clients can disconnect by sending a `ROOM_EXIT` (`r_e`) message: ``` { "t": "e_r" } ``` in which case the server will send a `ROOM_EXIT_SUCCESS` (`r_e_s`) message and close the connection: ``` { "f": "d1e313ee-9eac-47e5-9b9f-5daee88a8057", "m": { "t": "r_e_s", "d": { "rn": "batr" // room name } } } ``` The client can also just close the websocket connection. 😏 When a client disconnects, its positional data is removed from the room state and any remaining clients get a `ROOM_CLIENT_EXIT` (`r_c_e`) message telling them it's left: ``` { "f": "81270b00-2dde-4c33-ab4d-6cfc76b46c0c", "m": { "t": "r_c_e", "d": { "cId": "56252c73-2801-4fff-86f9-d214bdd52b94" } } } ``` ### In-room actions Once connected to a room, clients can send positional info, and interact with spheres in the room. All interactions are carried out using the following messages. #### `UPDATE_CLIENT_COORDS` (`u_c_c`) This is the most frequent message clients send, notifying a change in client position: ``` { "t": "u_c_c", "d": { "r": { // right "r": { // rotation "x": 9.234324, "y": 55.232423, "z": 7.234234 }, "p": { // position "x": 19.54384, "y": 25.3323, "z": 9.81832 } }, "l": { // left "r": { "x": 9.234324, "y": 55.232423, "z": 7.234234 }, "p": { "x": 19.54384, "y": 25.3323, "z": 9.81832 } }, "h": { // head "r": { "x": 9.234324, "y": 55.232423, "z": 7.234234 }, "p": { "z": 79.618, "y": 0, "x": 19.888 } } } } ``` These are broadcast to other clients in the same room as `ROOM_CLIENT_COORDS_UPDATED` (`r_c_c_u`), providing rotation (`r`) and position (`p`) coordinates for each of head (`h`), left hand (`l`) and right hand (`r`): ``` { "f": "41bd2e10-0f67-4b14-acee-84eeed1764b7", "m": { "t": "r_c_c_u", "d": { "r": { "r": { "x": 9.234324, "y": 55.232423, "z": 7.234234 }, "p": { "x": 19.54384, "y": 25.3323, "z": 9.81832 } }, "l": { "r": { "x": 9.234324, "y": 55.232423, "z": 7.234234 }, "p": { "x": 19.54384, "y": 25.3323, "z": 9.81832 } }, "h": { "r": { "x": 9.234324, "y": 55.232423, "z": 7.234234 }, "p": { "z": 79.618, "y": 0, "x": 19.888 } } } } } ``` Clients can optionally include sphere positions in the `UPDATE_CLIENT_COORDS` messages, for any spheres they currently hold (see below). To do so, they add an extra `spheres` object at the same level as the `head`, `left` and `right`: ``` { "f": "41bd2e10-0f67-4b14-acee-84eeed1764b7", "m": { "t": "r_c_c_u", "d": { "s": [ // spheres { "spId": "df0a769c-653d-44ec-9f17-e5a9d2c0f167", "p": { "x": 156, "y": 22, "z": 10 } }, { "spId": "69143b9a-1342-4aec-a7da-1daac29cee3d", "p": { "x": 2, "y": 3, "z": 4 } } ], "r": { [...] }, // right "l": { [...] }, // left "h": { [...] } // head } } } ``` Clients can interact with spheres in the room by sending messages as follows. #### `CREATE_SPHERE_OF_TONE_AT_POSITION` (`c_s_o_t_a_p`) ``` { "t": "c_s_o_t_a_p", "d": { "t": 0, // tone "p": { // position "x": 1, "y": 1, "z": 3 } } } ``` On successful creation, the server sends a `CREATE_SPHERE_SUCESS` (`c_s_s`) message: ``` { "f": "81270b00-2dde-4c33-ab4d-6cfc76b46c0c", // from "m": { "t": "c_s_s", "d": { "spId": "cad0a7d4-a055-49b2-a3f8-4e08d636dbce" } } } ``` #### `GRAB_SPHERE` (`g_s`) ``` { "t": "g_s", "d": { "spId": "090253c9-1c68-44c2-8aac-dba707c6aef7" } } ``` On successful grab, server sends `GRAB_SPHERE_SUCCESS` (`g_s_s`): ``` { "f": "81270b00-2dde-4c33-ab4d-6cfc76b46c0c", "m": { "t": "g_s_s", "d": { "spId": "31654801-9e61-4a5e-bdd6-a29e8ecd1001" } } } ``` Before accepting a grab, the server checks its state to see whether the sphere exists and whether it's already held by any other clients. Any problems are communicated back to the client. Possible errors are: `NON_EXISTENT_SPHERE` (`n_e_s`) ``` { "f": "81270b00-2dde-4c33-ab4d-6cfc76b46c0c", "m": { "t": "n_e_s", "d": { "spId": "66b93b19-03d6-4220-b44f-5b33d5d59269" // sphereId } } } ``` `CLIENT_HOLDING_SPHERE` (`c_h_s`): ``` { "f": "81270b00-2dde-4c33-ab4d-6cfc76b46c0c", "m": { "t": "c_h_s", "d": { "spId": "29312c56-b1a5-4530-8c7b-2c968e158683", "hId": "1eec65a7-2c9a-4fe4-a998-55d3f1754f21" // holderId - same as client in this case } } } ``` `SPHERE_ALREADY_HELD` (`s_a_h`) by another client: ``` { "f": "81270b00-2dde-4c33-ab4d-6cfc76b46c0c", "m": { "t": "s_a_h", "d": { "spId": "66b93b19-03d6-4220-b44f-5b33d5d59269", "hId": "055fa916-5e99-44dd-9e23-2f9323bb6f4c" } } } ``` `CLIENT_HOLDING_MAX_SPHERES` (`c_h_m_s`) - a client can only hold one sphere in each hand: ``` { "f": "81270b00-2dde-4c33-ab4d-6cfc76b46c0c", "m": { "t": "c_h_m_s", "d": {} } } ``` A successful grab creates a "hold" on the sphere, stopping other clients from acting on the sphere, and lasting until either the client sends a `RELEASE_SPHERE` (`r_s`) message, or is inactive (sends no messages relating to the held sphere) for a period defined in `roomConstants.SPHERE_INFO.SPHERE_HOLD_TIMEOUT`, in the file `src/rooms/room-data-constants.js`, currently set to 10 seconds. If the hold times out, the server sends a `SPHERE_HOLD_TIMEOUT` (`s_h_t`) to the client: ``` { "f": "81270b00-2dde-4c33-ab4d-6cfc76b46c0c", "m": { "t": "s_h_t", "d": { "spId": "922fbed6-5c57-4c14-bf0f-080abf5fa285" } } } ``` and a `ROOM_SPHERE_RELEASED` (`r_s_r`) message to any other clients in the room: ``` { "f": "81270b00-2dde-4c33-ab4d-6cfc76b46c0c", "m": { "t": "r_s_r", "d": { "spId": "922fbed6-5c57-4c14-bf0f-080abf5fa285", "cId": "1fe992a5-05d9-4791-8482-c598f31dbe22" } } } ``` #### `RELEASE_SPHERE` (`r_s`) ``` { "t": "r_s", "d": { "spId": "6f0e19bd-2451-43a0-be9d-b6e208e65ee2" } } ``` On receiving a `RELEASE_SPHERE` message, the server checks that the client is holding it, and if so sends a `RELEASE_SPHERE_SUCCESS` (`r_sp_s`) message to the client: ``` { "f": "81270b00-2dde-4c33-ab4d-6cfc76b46c0c", "m": { "t": "r_sp_s", "d": { "spId": "a62cabcd-e7d9-4687-beac-0c6c7fb9b953" } } } ``` and a `ROOM_SPHERE_RELEASED` (`r_s_r`) message to any other clients in the room: ``` { "f": "81270b00-2dde-4c33-ab4d-6cfc76b46c0c", "m": { "t": "r_s_r", "d": { "spId": "a62cabcd-e7d9-4687-beac-0c6c7fb9b953", "cId": "d8f59526-60b6-47ee-ab86-f39f441b8aaa" // released by this client } } } ``` Possible errors are `NON_EXISTENT_SPHERE` (as in `GRAB_SPHERE` above) and `RELEASE_SPHERE_INVALID` (`r_sp_i`): ``` { "f": "d1e313ee-9eac-47e5-9b9f-5daee88a8057", "m": { "t": "r_sp_i", "d": { "spId": "9c674bab-e2a6-4588-beff-2342d10a97b0" } } } ``` #### `DELETE_SPHERE` (`d_s`) ``` { "t": "d_s", "d": { "spId": "cfa16485-51e5-4db0-b54a-1cab6b6f003f" } } ``` On successful delete the server sends `DELETE_SPHERE_SUCCESS` (`d_s_s`): ``` { "f": "d1e313ee-9eac-47e5-9b9f-5daee88a8057", "m": { "t": "d_s_s", "d": { "spId": "cfa16485-51e5-4db0-b54a-1cab6b6f003f" } } } ``` and notifies any other clients with a `ROOM_SPHERE_DELETED` (`r_s_d`) message: ``` { "f": "5e44fd4e-54cb-42d7-ac41-f381a3844fee", "m": { "t": "r_s_d", "d": { "spId": "9de12a8d-96a4-42e8-842e-4230db33b306", "cId": "5e44fd4e-54cb-42d7-ac41-f381a3844fee" } } } ``` Possible errors include `NON_EXISTENT_SPHERE` (as above), `DELETE_SPHERE_DENIED` (`d_s_d`) if another client is holding the sphere: ``` { "f": "d1e313ee-9eac-47e5-9b9f-5daee88a8057", "m": { "t": "d_s_d", "d": { "spId": "9de12a8d-96a4-42e8-842e-4230db33b306", "hId": "9847a6ac-ae7c-43a8-8599-83b3079c9f1a" } } } ``` #### `STRIKE_SPHERE` (`s_s`) ``` { "t": "s_s", "d": { "spId": "aabb5ecd-1662-4db5-91f9-6bd1d8f2b0f9", "v": 0.5 } } ``` Sphere strikes are broadcast to other clients in the room via a `ROOM_SPHERE_STRUCK` (`r_s_s`) message: ``` { "f": "1a3823f3-79e6-4cd1-9e33-2044d16da300", "m": { "t": "r_s_s", "d": { "spId": "aabb5ecd-1662-4db5-91f9-6bd1d8f2b0f9", "v": 0.5, "cId": "1a3823f3-79e6-4cd1-9e33-2044d16da300" } } } ``` Strikes sent to non-existent spheres are silently dropped. #### `SET_SPHERE_TONE` (`s_s_t`) ``` { "t": "s_s_t", "d": { "spId": "99c2989c-beba-4c3f-acb9-6f5fe5e4ec33", "t": 10 // tone } } ``` On successful set, the server sends a `SET_SPHERE_TONE_SUCCESS` (`s_s_t_s`) message: ``` { "f": "d1e313ee-9eac-47e5-9b9f-5daee88a8057", "m": { "t": "s_s_t_s", "d": { "spId": "99c2989c-beba-4c3f-acb9-6f5fe5e4ec33" } } } ``` and notifies other clients of the change in tone with a `ROOM_SPHERE_TONE_SET` (`r_s_t_s`) message: ``` { "f": "d1e313ee-9eac-47e5-9b9f-5daee88a8057", "m": { "t": "r_s_t_s", "d": { "spId": "cd3aa6cf-f15c-4d7a-be6e-d862912fbd62", "t": 10, // tone "cId": "13eb8894-330a-4e37-8ce3-645edf3c6773" } } } ``` Clients don't need to hold a sphere to set its tone, but if it's currently held by another client, the server will send a `SET_SPHERE_TONE_DENIED` (`s_s_t_d`) message: ``` { "f": "d1e313ee-9eac-47e5-9b9f-5daee88a8057", "m": { "t": "s_s_t_d", "d": { "spId": "99c2989c-beba-4c3f-acb9-6f5fe5e4ec33", "hId": "9e8e4e40-a523-4345-8e21-b274177fe073" } } } ``` As above, the server will send a `NON_EXISTENT_SPHERE` message if the specified sphere doesn't exist in the room. #### `SET_SPHERE_CONNECTIONS` (`s_s_c`) ``` { "t": "s_s_c", "d": { "spId": "973c742d-866f-4b25-9ae6-8830e7f13e59", "c": [ // connections "221c3e00-c0e9-4bd9-bdb3-187c5dcf5499", "03d6c324-73e0-4804-9b84-9b203a303a38" ] } } ``` As in `SET_SPHERE_TONE`, clients don't need to hold a sphere to set its connections, but if it's currently held by another client, the server will send a `SET_SPHERE_CONNECTIONS_DENIED` (`s_s_c_d`) message: ``` { "f": "d1e313ee-9eac-47e5-9b9f-5daee88a8057", "m": { "t": "s_s_c_d", "d": { "spId": "973c742d-866f-4b25-9ae6-8830e7f13e59", "hId": "7bf0b7a2-ee3e-4b76-9d68-675aa4e97f2a" } } } ``` As above, the server will send a `NON_EXISTENT_SPHERE` message if the specified sphere doesn't exist in the room. ================================================ FILE: backend/package.json ================================================ { "name": "webvr-musicalforest-backend", "version": "1.0.0", "description": "node.js websocket app server for forest.webvrexperiments.com", "main": "index.js", "scripts": { "start": "ENVIRONMENT_NAME=$(hostname) LOCAL_IP_ADDRESS=$(/sbin/ifconfig en0|grep -w inet|awk '{print $2}') PROJECT_ID=$(gcloud config list project --format json | jq -r .core.project) babel-node src/index.js", "build": "babel src -d dist", "clean": "rm -rf dist", "serve": "NODE_ENV=production ENVIRONMENT_NAME=$(hostname) LOCAL_IP_ADDRESS=$(/sbin/ifconfig en0|grep -w inet|awk '{print $2}') PROJECT_ID=$(gcloud config list project --format json | jq -r .core.project) node dist/index.js", "emulators": "parallelshell 'gcloud beta emulators datastore start --no-store-on-disk' 'gcloud beta emulators pubsub start'", "start_emulated": "$(gcloud beta emulators datastore env-init) && $(gcloud beta emulators pubsub env-init) && NODE_ENV=production npm start" }, "repository": { "type": "git", "url": "https://github.com/googlecreativelab/webvr-musicalforest/" }, "author": "Google Creative Lab", "license": "Apache-2.0", "devDependencies": { "babel-cli": "6.18.0", "babel-polyfill": "6.16.0", "babel-preset-es2015": "6.18.0", "babel-preset-stage-2": "6.18.0", "mocha": "3.2.0", "nodemon": "1.11.0", "parallelshell": "^2.0.0" }, "dependencies": { "@google-cloud/datastore": "0.7.0", "@google-cloud/logging": "0.7.0", "@google-cloud/pubsub": "0.8.0", "@google-cloud/storage": "0.7.0", "ajv": "4.10.0", "arrify": "1.0.1", "dateformat": "2.0.0", "express": "4.14.0", "fast-ratelimit": "2.0.6", "grpc": "1.0.1", "hashring": "3.2.0", "hashtable": "2.0.1", "http-proxy": "1.16.2", "javascript-state-machine": "2.4.0", "node-fetch": "1.6.3", "object-sizeof": "1.1.1", "redux": "3.6.0", "redux-actions": "1.1.0", "redux-saga": "0.14.3", "tracer": "0.8.7", "uuid": "3.0.1", "uws": "0.14.1" } } ================================================ FILE: backend/src/config.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import fs from 'fs'; import uuid from 'uuid'; import dateformat from 'dateformat'; import storage from '@google-cloud/storage'; import messageConstants from './messages/message-constants'; import serverConstants from './server/server-constants'; import { formatStringAsGcpResourceName } from './utils/string-utils'; const logError = ( errorMsg ) => { let ts = dateformat( new Date(), LOG_DATE_FORMAT ); console.error( `[${SHORT_SERVER_ID}|${ts}] => `, errorMsg ); }; /******************************************************************************* * REQUIRED ENVIRONMENT VARIABLES *******************************************************************************/ // nothing will work without a GCP PROJECT_ID if( !process.env.PROJECT_ID ) { logError( `PROJECT_ID not specified, exiting.` ); process.exit( serverConstants.ERROR_TYPES.BAD_OS_EXIT_ERROR_CODE ); } const PROJECT_ID = process.env.PROJECT_ID; // nothing will work without a LOCAL_IP_ADDRESS if( !process.env.LOCAL_IP_ADDRESS ) { logError( `LOCAL_IP_ADDRESS not specified, exiting.` ); process.exit( serverConstants.ERROR_TYPES.BAD_OS_EXIT_ERROR_CODE ); } const LOCAL_IP_ADDRESS = process.env.LOCAL_IP_ADDRESS; // nothing will work without an ENVIRONMENT_NAME if( !process.env.ENVIRONMENT_NAME ) { logError( `ENVIRONMENT_NAME not specified, exiting.` ); process.exit( serverConstants.ERROR_TYPES.BAD_OS_EXIT_ERROR_CODE ); }; const ENVIRONMENT_NAME = process.env.ENVIRONMENT_NAME; /******************************************************************************* * LOG FORMAT INFO *******************************************************************************/ // generate a server id const SERVER_ID = uuid(); const SHORT_SERVER_ID = SERVER_ID.split( '-' ).slice(0, 2).join( '-' ); const LOG_DATE_FORMAT = "yyyymmdd|HH:MM:ss.l"; /******************************************************************************* * PUBSUB SYNC CHANNEL INFO *******************************************************************************/ // setup PubSub sync channel topic name, distinct per execution group // e.g. 'gce', 'gke', 'mymac.config' const syncTopicNameComponents = [ serverConstants.SYNC_INFO.SYNC_TOPIC_NAME, '-', ENVIRONMENT_NAME ]; const SYNC_TOPIC_NAME = formatStringAsGcpResourceName( syncTopicNameComponents.join( '' ) ); // only log to Stackdriver explicitly if requested const LOG_TO_CLOUD = process.env.LOG_TO_CLOUD === 'true' ? true : false; /******************************************************************************* * NETWORK IP/PORT INFO *******************************************************************************/ // set up some defaults const WS_SERVER_PORT = process.env.WS_SERVER_PORT ? Number( process.env.WS_SERVER_PORT ) : 8100; const WS_BALANCER_PORT = process.env.WS_BALANCER_PORT ? Number( process.env.WS_BALANCER_PORT ) : 9100; const MAX_CLIENTS_PER_ROOM = process.env.MAX_CLIENTS_PER_ROOM ? Number( process.env.MAX_CLIENTS_PER_ROOM ) : 10; // construct balancer identifier const LOCAL_IP_PORT_STRING = `${LOCAL_IP_ADDRESS}:${WS_SERVER_PORT}`; /******************************************************************************* * SSL INFO FOR HOSTING ON GKE *******************************************************************************/ // GKE deploys provide USE_SSL=true in the environment const USE_SSL = process.env.USE_SSL === "true" ? true : false; // tell the app whether to serve SSL let SSL_INFO = { useSsl: USE_SSL }; // if so, set up the certificate info if( USE_SSL === true ) { // make sure there's a specified certificate name if( !process.env.SSL_CERT_HOST_NAME ) { logError( `USE_SSL=true requires a specified SSL_CERT_HOST_NAME` ); process.exit( serverConstants.ERROR_TYPES.BAD_OS_EXIT_ERROR_CODE ); } // prepare for export, set up GCS bucket/file info SSL_INFO.sslCertHostName = process.env.SSL_CERT_HOST_NAME; SSL_INFO.sslStorageBucketName = `${PROJECT_ID}-ssl`; SSL_INFO.privKeyFileName = `${process.env.SSL_CERT_HOST_NAME}/privkey.pem`; SSL_INFO.fullChainFileName = `${process.env.SSL_CERT_HOST_NAME}/fullchain.pem`; } /******************************************************************************* * ROOM CHOICE THRESHOLD CONFIGS *******************************************************************************/ // provide shortcuts for headset type constants const HT_3DOF = messageConstants.HEADSET_TYPES.HEADSET_TYPE_3DOF; const HT_6DOF = messageConstants.HEADSET_TYPES.HEADSET_TYPE_6DOF; const HT_VIEWER = messageConstants.HEADSET_TYPES.HEADSET_TYPE_VIEWER; // set up headset threshold rules const HEADSET_RULES = { [ HT_3DOF ]: { threshold: 3 }, [ HT_6DOF ]: { threshold: 3 }, [ HT_VIEWER ]: { threshold: 10 } }; /******************************************************************************* * CHOICE OF WHETHER TO TIMEOUT SPHERE HOLDS AUTOMATICALLY *******************************************************************************/ const TIMEOUT_SPHERE_HOLDS = process.env.TIMEOUT_SPHERE_HOLDS === 'false' ? false : true;; /******************************************************************************* * CHOICE OF WHETHER TO DROP VIEWER MESSAGES *******************************************************************************/ const DROP_VIEWER_MESSAGES_IN_PRODUCTION = process.env.DROP_VIEWER_MESSAGES_IN_PRODUCTION === 'true' ? true : false; /******************************************************************************* * CHOICE OF WHETHER TO RATE LIMIT CLIENT/SPHERE POSITION UPDATES *******************************************************************************/ const PER_CLIENT_RATE_LIMIT = process.env.PER_CLIENT_RATE_LIMIT === 'false' ? false : true; const PER_MESSAGE_TYPE_RATE_LIMIT = process.env.PER_MESSAGE_TYPE_RATE_LIMIT === 'false' ? false : true; const RATE_LIMIT_INFO = { perClientRateLimit: PER_CLIENT_RATE_LIMIT, // switch per-clientrate limiting on or off perMessageTypeRateLimit: PER_MESSAGE_TYPE_RATE_LIMIT, // switch rate limiting on or off perTypeMsgThreshold: 1, // 1 message of each type per client perTypeMsgTtl: 0.2, // per 200ms perClientMsgThreshold: 20, // 20 message of any type per client perClientMsgTtl: 1 // per 1s }; /******************************************************************************* * NAME OF PRODUCTION ENVIRONMENT GCP PROJECT *******************************************************************************/ const PRODUCTION_ENVIRONMENT_PROJECT_ID = ''; const PRODUCTION_ENVIRONMENT_REQUIRED_ORIGIN = ''; /******************************************************************************* * EXPORT CONFIG *******************************************************************************/ export default { environmentName: ENVIRONMENT_NAME, syncTopicName: SYNC_TOPIC_NAME, logDateFormat: LOG_DATE_FORMAT, logToCloud: LOG_TO_CLOUD, localIpAddress: LOCAL_IP_ADDRESS, localIpPortString: LOCAL_IP_PORT_STRING, serverPort: WS_SERVER_PORT, balancerPort: WS_BALANCER_PORT, projectId: PROJECT_ID, dropViewerMessagesInProduction: DROP_VIEWER_MESSAGES_IN_PRODUCTION, productionEnvironmentProjectId: PRODUCTION_ENVIRONMENT_PROJECT_ID, productionEnvironmentRequiredOrigin: PRODUCTION_ENVIRONMENT_REQUIRED_ORIGIN, maxClientsPerRoom: MAX_CLIENTS_PER_ROOM, clientRoomJoinDeadlineInMs: 5000, roomHeartbeatDelayInMs: 5000, serverId: SERVER_ID, shortServerId: SHORT_SERVER_ID, sslInfo: SSL_INFO, headsetRules: HEADSET_RULES, rateLimitInfo: RATE_LIMIT_INFO, timeoutSphereHolds: TIMEOUT_SPHERE_HOLDS }; ================================================ FILE: backend/src/datastore/datastore-constants.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const DATASTORE = { // entity types DS_ENTITY_KEY_SERVER_SUBSCRIPTION_INFO: 'ServerSubscriptionInfo', DS_ENTITY_KEY_PER_ENVIRONMENT_RATE_LIMIT_INFO: 'PerEnvironmentRateLimitInfo' }; export default DATASTORE; ================================================ FILE: backend/src/datastore/index.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import datastore from '@google-cloud/datastore'; import config from '../config'; import { logger } from '../logger'; import datastoreConstants from './datastore-constants'; // create a module-wrapped datastore client object at startup let dsClient = ( ( conf ) => { if( !dsClient ) { if( process.env.DATASTORE_EMULATOR_HOST ) { logger.info( `using Datastore emulator running at ${process.env.DATASTORE_EMULATOR_HOST}` ); } else { logger.info( `using live Datastore on GCP` ); } dsClient = datastore( conf ); } return dsClient; })( config ); const runQuery = function* ( query ) { return yield dsClient.runQuery( query ); }; const save = function* ( entities ) { return yield dsClient.save( entities ); }; const key = function ( keyComponents ) { return dsClient.key( keyComponents ); }; // export functions to the default namespace, to make a 'datastore' import for other modules export default { runQuery, save, key }; // export constants individually, to make a { datastoreConstants } import for other modules export { datastoreConstants, dsClient } ================================================ FILE: backend/src/index.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import "babel-polyfill"; import config from './config'; import { dispatchStoreAction } from './store'; import { logger } from './logger'; import { serverActions } from './server'; // create a server setup request action let setupAction = serverActions.startServerSetupRequestAction(); /* * dispatch the setup request to the store. * all the action now happens via setupServer() in * server/server-sagas.js */ dispatchStoreAction( setupAction ); ================================================ FILE: backend/src/logger/cloud-logger.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import Logging from '@google-cloud/logging'; import config from '../config'; import { formatStringAsGcpResourceName } from '../utils/string-utils'; // create a client const loggingClient = Logging( config ); // the name of the log to write to const logName = formatStringAsGcpResourceName( `${config.environmentName}-cloud-app-log` ); // choose a log to write to const selectedLog = loggingClient.log( logName ); // set up resource metadata const metadata = { resource: { type: 'project', labels: { project_id: config.projectId } } }; // log function wrapper const writeCloudLogEntry = ( tracerData ) => { const entry = selectedLog.entry( metadata, tracerData.output ); selectedLog.write(entry).catch( ( error ) => { console.error( `error logging message: ${error.message}` ); } ); }; export { writeCloudLogEntry }; ================================================ FILE: backend/src/logger/index.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import tracer from 'tracer'; import config from '../config'; import { writeCloudLogEntry } from './cloud-logger'; let selectedConsole = config.logToCloud ? tracer.console : tracer.colorConsole; let consoleOptions = { format: "[{{shortServerId}}|{{timestamp}}] {{sigil}} {{message}} [{{file}}:{{line}}]", dateformat: config.logDateFormat, preprocess: (data) => { data.shortServerId = config.shortServerId; data.localIpAddress = config.localIpAddress; data.sigil = { error: `=>`, warn: `**`, info: `->`, debug: `||`, trace: `##`, log: `` }[ data.title ]; } }; // only do info() and above in production if( config.projectId === config.productionEnvironmentProjectId ) { consoleOptions.level = 'info'; consoleOptions.format = "[{{shortServerId}}|{{localIpAddress}}|{{timestamp}}] {{sigil}} {{message}} [{{file}}:{{line}}]"; } // write log via Stackdriver API? if( config.logToCloud ) { consoleOptions.transport = writeCloudLogEntry; } let l = selectedConsole( consoleOptions ); let logger = { error: l.error, warn: l.warn, info: l.info, debug: l.debug, trace: l.trace, log: l.log }; export { logger }; ================================================ FILE: backend/src/messages/index.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import messageActions from './message-actions'; import messageConstants from './message-constants'; import messageHandler from './message-handler'; import messageSagas from './message-sagas'; import messageValidator from './message-validator'; export { messageActions, messageConstants, messageHandler, messageSagas, messageValidator }; ================================================ FILE: backend/src/messages/message-actions.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import messageConstants from './message-constants'; import { messageSchema } from './message-schema'; /******************************************************************************* * * reference of data structure from JSON schema * * { * definitions: { * ..., * room_client_position_update: * { type: 'object', * properties: [Object], * required: [Object] } * }, * * properties: { * ..., * room_client_position_update: * { '$ref': '#/definitions/room_client_position_update' } * } * } * * room_client_position_update: { type: { type: 'string', minLength: 29, maxLength: 29 }, * data: * { type: 'object', * properties: * { head: [Object], * left: [Object], * right: [Object], * userdata: [Object] }, * required: [ 'head', 'left', 'right' ] } } * ********************************************************************************/ /******************************************************************************* * see https://github.com/acdlite/redux-actions * * redux-actions generates action request objects for use as triggers in redux. * * example: * { * type: 'INCREMENT', * payload: 42, * otherVar: true * } * *******************************************************************************/ // generates 'redux-actions' action functions for all incoming message types in schema const messageRequestActions = Object.keys( messageSchema.properties ).reduce( // for each key in messageSchema.properties ( acc, type ) => { // create a function which ... let fn = function() { // combines the provided type with fn args to generate an action request object return Object.assign( { type }, arguments[ 0 ] ); }; // key the function by the incoming message type name, to export them all acc[ type ] = fn; return acc; }, {} // empty starting accumulator ); // export the generated functions for use as "import messageActions from 'message-actions'" export default messageRequestActions; ================================================ FILE: backend/src/messages/message-constants.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const sphereIdLabel = 'spId'; const clientIdLabel = 'cId'; const holderIdLabel = 'hId'; const serverIdLabel = 'srId'; const toneLabel = 't'; const positionLabel = 'p'; const rotationLabel = 'r'; const headLabel = 'h'; const leftLabel = 'l'; const rightLabel = 'r'; const sphereLabel = 's'; const headsetTypeLabel = 'ht'; const velocityLabel = 'v'; const connectionsLabel = 'c'; const meristemLabel = 'm'; const fromLabel = 'f'; const typeLabel = 't'; const dataLabel = 'd'; const msgLabel = 'm'; const roomNameLabel = 'rn'; const soundbankLabel = 'sb'; const clientsLabel = 'c'; const spheresLabel = 's'; const INCOMING_MESSAGE_TYPES = { EXIT_ROOM: 'e_r', UPDATE_CLIENT_COORDS: 'u_c_c', // position msg for individual client CREATE_SPHERE_OF_TONE_AT_POSITION: 'c_s_o_t_a_p', GRAB_SPHERE: 'g_s', RELEASE_SPHERE: 'r_s', DELETE_SPHERE: 'd_s', STRIKE_SPHERE: 's_s', SET_SPHERE_TONE: 's_s_t', SET_SPHERE_CONNECTIONS: 's_s_c' }; const INCOMING_MESSAGE_COMPONENTS = { ALL_MESSAGES: { FROM: fromLabel, MSG: msgLabel, TYPE: typeLabel, DATA: dataLabel }, REFERENCES: { COORDINATE: 'coordinate', COORDINATE_SET: 'coordinateSet' }, REF_COORDINATE: { X: 'x', Y: 'y', Z: 'z' }, REF_COORDINATE_SET: { POSITION: positionLabel, ROTATION: rotationLabel }, UPDATE_CLIENT_COORDS: { HEAD: headLabel, LEFT: leftLabel, RIGHT: rightLabel, SPHERES: spheresLabel, SPHERE_ID: sphereIdLabel, SPHERE_POSITION: positionLabel }, CREATE_SPHERE_OF_TONE_AT_POSITION: { TONE: toneLabel, POSITION: positionLabel }, GRAB_SPHERE: { SPHERE_ID: sphereIdLabel }, RELEASE_SPHERE: { SPHERE_ID: sphereIdLabel }, DELETE_SPHERE: { SPHERE_ID: sphereIdLabel }, STRIKE_SPHERE: { SPHERE_ID: sphereIdLabel, VELOCITY: velocityLabel }, SET_SPHERE_TONE: { SPHERE_ID: sphereIdLabel, TONE: toneLabel }, SET_SPHERE_CONNECTIONS: { SPHERE_ID: sphereIdLabel, CONNECTIONS: connectionsLabel } }; const OUTGOING_MESSAGE_TYPES = { CONNECTION_INFO: 'c_i', // sent to new clients on connect ROOM_STATUS_INFO: 'r_s_i', // sent to new clients on joining a room ROOM_EXIT_SUCCESS: 'r_e_s', // sent to clients leaving a room ROOM_HEARTBEAT: 'r_h', // server heartbeat message for a room ROOM_CLIENT_JOIN: 'r_c_j', // sent to existing clients on client join ROOM_CLIENT_EXIT: 'r_c_e', // sent to remaining clients on client exit ROOM_CLIENT_COORDS_UPDATED: 'r_c_c_u', ROOM_SPHERE_CREATED: 'r_s_c', ROOM_SPHERE_GRABBED: 'r_s_g', ROOM_SPHERE_POSITION_UPDATED: 'r_s_p_u', ROOM_SPHERE_TONE_SET: 'r_s_t_s', ROOM_SPHERE_CONNECTIONS_SET: 'r_s_c_s', ROOM_SPHERE_RELEASED: 'r_s_r', ROOM_SPHERE_STRUCK: 'r_s_s', ROOM_SPHERE_DELETED: 'r_s_d', CREATE_SPHERE_DENIED: 'c_s_d', CREATE_SPHERE_SUCCESS: 'c_s_s', GRAB_SPHERE_DENIED: 'g_s_d', GRAB_SPHERE_SUCCESS: 'g_s_s', RELEASE_SPHERE_DENIED: 'r_sp_d', RELEASE_SPHERE_INVALID: 'r_sp_i', RELEASE_SPHERE_SUCCESS: 'r_sp_s', DELETE_SPHERE_DENIED: 'd_s_d', DELETE_SPHERE_INVALID: 'd_s_i', DELETE_SPHERE_SUCCESS: 'd_s_s', SET_SPHERE_TONE_DENIED: 's_s_t_d', SET_SPHERE_TONE_INVALID: 's_s_t_i', SET_SPHERE_TONE_SUCCESS: 's_s_t_s', SET_SPHERE_CONNECTIONS_DENIED: 's_s_c_d', SET_SPHERE_CONNECTIONS_INVALID: 's_s_c_i', SET_SPHERE_CONNECTIONS_SUCCESS: 's_s_c_s', CONNECT_SPHERES_IDENTICAL: 'c_s_id', CONNECT_SPHERES_INVALID: 'c_s_in', CONNECT_SPHERES_MISSING: 'c_s_m' }; const OUTGOING_MESSAGE_COMPONENTS = { ALL_MESSAGES: { DATA: dataLabel, FROM: fromLabel, MSG: msgLabel, TYPE: typeLabel, CLIENT_ID: clientIdLabel }, CONNECTION_INFO: { CLIENT_ID: clientIdLabel, SERVER_ID: serverIdLabel }, ROOM_STATUS_INFO: { ROOM_NAME: roomNameLabel, SOUNDBANK: soundbankLabel, CLIENTS: clientsLabel, CLIENT_ID: clientIdLabel, CLIENT_HEADSET_TYPE: headsetTypeLabel, SPHERES: spheresLabel, SPHERE_ID: sphereIdLabel, TONE: toneLabel, POSITION: positionLabel, CONNECTIONS: connectionsLabel, MERISTEM: meristemLabel }, ROOM_EXIT_SUCCESS: { ROOM_NAME: roomNameLabel }, CREATE_SPHERE_SUCCESS: { SPHERE_ID: sphereIdLabel }, SPHERE_ACTION_DENIED: { SPHERE_ID: sphereIdLabel, HOLDER_ID: holderIdLabel, }, ROOM_SPHERE_CREATED: { SPHERE_ID: sphereIdLabel, TONE: toneLabel, POSITION: positionLabel, CLIENT_ID: clientIdLabel }, GRAB_SPHERE_SUCCESS: { SPHERE_ID: sphereIdLabel, CLIENT_ID: clientIdLabel }, RELEASE_SPHERE_SUCCESS: { SPHERE_ID: sphereIdLabel, CLIENT_ID: clientIdLabel }, SPHERE_HOLD_TIMEOUT: { SPHERE_ID: sphereIdLabel, }, ROOM_SPHERE_RELEASED: { SPHERE_ID: sphereIdLabel, CLIENT_ID: clientIdLabel }, ROOM_CLIENT_COORDS_UPDATED: { HEAD: headLabel, LEFT: leftLabel, RIGHT: rightLabel, SPHERES: sphereLabel }, ROOM_SPHERE_POSITION_UPDATED: { SPHERE_ID: sphereIdLabel, POSITION: positionLabel, CLIENT_ID: clientIdLabel }, SET_SPHERE_TONE_SUCCESS: { SPHERE_ID: sphereIdLabel }, ROOM_SPHERE_TONE_SET: { SPHERE_ID: sphereIdLabel, TONE: toneLabel, CLIENT_ID: clientIdLabel }, SET_SPHERE_CONNECTIONS_SUCCESS: { SPHERE_ID: sphereIdLabel }, ROOM_SPHERE_CONNECTIONS_SET: { SPHERE_ID: sphereIdLabel, CONNECTIONS: connectionsLabel, CLIENT_ID: clientIdLabel }, DELETE_SPHERE_SUCCESS: { SPHERE_ID: sphereIdLabel, CLIENT_ID: clientIdLabel }, ROOM_SPHERE_DELETED: { SPHERE_ID: sphereIdLabel, CLIENT_ID: clientIdLabel }, ROOM_SPHERE_STRUCK: { SPHERE_ID: sphereIdLabel, CLIENT_ID: clientIdLabel, VELOCITY: velocityLabel }, ROOM_CLIENT_JOIN: { CLIENT_ID: clientIdLabel, CLIENT_HEADSET_TYPE: headsetTypeLabel }, ROOM_CLIENT_EXIT: { CLIENT_ID: clientIdLabel }, ROOM_HEARTBEAT: { COUNT: 'c', SECONDS: 's' } }; const HEADSET_TYPES = { HEADSET_TYPE_3DOF: '3dof', HEADSET_TYPE_6DOF: '6dof', HEADSET_TYPE_VIEWER: 'viewer' }; const ERROR_TYPES = { INVALID_URL: 'i_u', NO_ROOMS_AVAILABLE: 'n_r_a', NOT_IN_ROOM: 'n_i_r', NO_SUCH_ROOM: 'n_s_r', ROOM_QUEUE_FULL: 'r_q_f', ROOM_FULL: 'r_f', ROOM_UNAVAILABLE: 'r_u', BUSY_TRY_AGAIN: 'b_t_a', ROOM_NOT_READY: 'r_n_r', ROOM_JOIN_TIMEOUT: 'r_j_t', ALREADY_IN_ROOM: 'a_i_r', ALREADY_IN_ROOM_QUEUE: 'a_i_r_q', CREATE_SPHERE_UNAVAILABLE: 'c_s_u', NON_EXISTENT_SPHERE: 'n_e_s', SPHERE_ALREADY_HELD: 's_a_h', CLIENT_HOLDING_SPHERE: 'c_h_s', CLIENT_HOLDING_MAX_SPHERES: 'c_h_m_s', TOO_MANY_SPHERE_CONNECTIONS: 't_m_s_c', GRAB_SPHERE_ERROR: 'g_s_e', RELEASE_SPHERE_ERROR: 'r_s_e', DELETE_SPHERE_ERROR: 'd_s_e', CONNECT_SPHERE_ERROR: 'c_s_e', SPHERE_HOLD_TIMEOUT: 's_h_t', CLIENT_INACTIVITY_TIMEOUT: 'c_i_t', SYSTEM_ERROR: 's_' }; export default { INCOMING_MESSAGE_TYPES, INCOMING_MESSAGE_COMPONENTS, OUTGOING_MESSAGE_TYPES, OUTGOING_MESSAGE_COMPONENTS, HEADSET_TYPES, ERROR_TYPES }; export { HEADSET_TYPES }; ================================================ FILE: backend/src/messages/message-handler.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { roomDataActions, roomContentConstants, roomDataConstants, roomStateConstants } from '../rooms'; import { logger } from '../logger'; import { roomStateActions } from '../rooms' import { serialize } from '../s11n'; import { sendWsMessageWithLogger } from '../utils/websocket-utils'; import { dispatchStoreAction, getStoreState } from '../store'; import config from '../config'; import messageActions from './message-actions'; import messageConstants from './message-constants'; import { perMessageTypeRateLimiter } from './message-rate-limiter'; import messageValidator from './message-validator'; const messageComponents = messageConstants.INCOMING_MESSAGE_COMPONENTS; /******************************************************************************* * HANDLER FOR INCOMING ROOM MESSAGES FROM THIS SERVER'S WEBSOCKET CLIENTS *******************************************************************************/ /* * this function validates an incoming message against the JSON message schema, * and dispatches successfully validated messages to the appropriate handler * defined in messageActions, generated from the message types in the schema. */ const handleWebsocketMessage = ( ws, message ) => { // drop empty messages if( typeof message === 'undefined' || message === '' || message === null ) { return; } // make sure the client's in a room if( typeof ws.currentRoom === 'undefined' ) { return; } // make sure message format is valid, drop any invalid messages let validatedMessage; try { validatedMessage = messageValidator.validateMessage( message ); } catch( error ) { logger.debug( `error validating incoming message from client ${ws.id}: ${error.message}` ); return; } // messageActions exports action functions generated from schema, hence keyed the same let validatedMessageType = validatedMessage[ messageComponents.ALL_MESSAGES.TYPE ]; // if rate limiting's switched off if( config.rateLimitInfo.perMessageTypeRateLimit === false ) { // handle the message straightaway handleValidatedMessageOfTypeForWebsocket( validatedMessage, validatedMessageType, ws ); return; } // otherwise, rate-limit messages per type, per client const rateLimiterNamespace = `${ws.id}/${validatedMessageType}`; perMessageTypeRateLimiter.consume( rateLimiterNamespace ) // as long as the client is within limits for this message type .then( () => { // handle the message handleValidatedMessageOfTypeForWebsocket( validatedMessage, validatedMessageType, ws ); } ) // otherwise .catch( ( error ) => { // silently drop the message } ); }; const handleValidatedMessageOfTypeForWebsocket = ( validatedMessage, validatedMessageType, ws ) => { // add id to msg, dispatch generated messageAction required by message type let attributedMessage = { ws, [ messageComponents.ALL_MESSAGES.FROM ]: ws.id, [ messageComponents.ALL_MESSAGES.MSG ]: validatedMessage }; // tell the message saga which room it's in attributedMessage.roomName = ws.currentRoom; // check the room is in the right state to receive messages let roomState = getStoreState().roomStateReducer.rooms[ ws.currentRoom ]; // rooms need to be in specific states to accept messages let roomStatesAcceptingMessages = [ roomStateConstants.STATES.READY, roomStateConstants.STATES.ROOM_FULL ]; if( roomStatesAcceptingMessages.indexOf( roomState.status ) < 0 ) { let roomNotReadyMessage = { from: config.serverId, msg: { type: messageConstants.ERROR_TYPES.ROOM_NOT_READY } }; sendWsMessageWithLogger( ws, roomNotReadyMessage, logger ); return; } dispatchStoreAction( messageActions[ validatedMessageType ]( attributedMessage ) ); }; export default { handleWebsocketMessage }; ================================================ FILE: backend/src/messages/message-rate-limiter.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { FastRateLimit } from 'fast-ratelimit'; import config from '../config'; import { logger } from '../logger'; // rate-limits overall messages per client, regardless of type let perClientRateLimiter = new FastRateLimit({ threshold: config.rateLimitInfo.perClientMsgThreshold, ttl: config.rateLimitInfo.perClientMsgTtl }); if( config.rateLimitInfo.perClientRateLimit ) { logger.info( `default overall client message rate limiter allowing ${perClientRateLimiter.__options.threshold}` + `msg/${perClientRateLimiter.__options.ttl_millisec}ms` ); } // rate-limits individual message types per-client let perMessageTypeRateLimiter = new FastRateLimit({ threshold: config.rateLimitInfo.perTypeMsgThreshold, ttl: config.rateLimitInfo.perTypeMsgTtl }); if( config.rateLimitInfo.perMessageTypeRateLimit ) { logger.info( `default per-type client message rate limiter allowing ${perMessageTypeRateLimiter.__options.threshold}` + `msg/${perMessageTypeRateLimiter.__options.ttl_millisec}ms` ); } // allows dynamic setting of per-client rate limiter threshold const setPerClientRateLimiterOptions = ( options ) => { perClientRateLimiter.__options = options; logger.info( `set overall client message rate limiter to allow ${perClientRateLimiter.__options.threshold}` + `msg/${perClientRateLimiter.__options.ttl_millisec}ms` ); }; // allows dynamic setting of per-message-type rate limiter threshold const setPerMessageTypeRateLimiterOptions = ( options ) => { perMessageTypeRateLimiter.__options = options; logger.info( `set per-type client message rate limiter to allow ${perMessageTypeRateLimiter.__options.threshold}` + `msg/${perMessageTypeRateLimiter.__options.ttl_millisec}ms` ); }; export { perClientRateLimiter, perMessageTypeRateLimiter, setPerClientRateLimiterOptions, setPerMessageTypeRateLimiterOptions }; ================================================ FILE: backend/src/messages/message-sagas.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import uuid from 'uuid'; import { delay, takeEvery, takeLatest } from 'redux-saga'; import { call, put, fork, select } from 'redux-saga/effects'; import { logger } from '../logger'; import { sphereConstants } from '../spheres'; import { roomDataActions, roomNames, roomStateActions, roomStateConstants } from '../rooms'; import { makeWsReplyMessage, makeWsBroadcastMessage, sendWsMessageWithLogger } from '../utils/websocket-utils'; import config from '../config'; import { sagaUtils } from '../utils'; import messageActions from './message-actions'; import messageConstants from './message-constants'; const incomingMsgComponents = messageConstants.INCOMING_MESSAGE_COMPONENTS; const outgoingMsgComponents = messageConstants.OUTGOING_MESSAGE_COMPONENTS; /******************************************************************************* * EXIT ROOM *******************************************************************************/ const exitRoom = function* ( message ) { logger.trace( `client ${message.ws.id} is leaving room ${message.ws.currentRoom}` ); // send a goodbye message to the client let roomExitSuccessMessage = makeWsReplyMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.ROOM_EXIT_SUCCESS, { [ outgoingMsgComponents.ROOM_EXIT_SUCCESS.ROOM_NAME ]: message.ws.currentRoom } ); sendWsMessageWithLogger( message.ws, roomExitSuccessMessage, logger ); // remove it from the state yield put( roomDataActions.removeLocalClientFromRoomRequestAction( message.ws.id, message.ws.currentRoom ) ); // close the connection message.ws.close(); }; /******************************************************************************* * UPDATE ROOM CLIENT POSITION *******************************************************************************/ const updateClientCoords = function* ( action ) { let { msgType, msgData } = getTypeAndDataForMessage( action ); // check whether the coords include any sphere position update let spherePositionUpdates = msgData[ incomingMsgComponents.UPDATE_CLIENT_COORDS.SPHERES ]; if( typeof spherePositionUpdates !== 'undefined' ) { let roomState = yield select( ( state ) => { return state.roomDataReducer.rooms; } ); let spheresHeldByClient = roomState[ action.roomName ].content.clients[ action.ws.id ].spheresHeld; let checkedSpherePositionUpdates = []; // spherePositionUpdates will be an array if it exists, as it's been validated const spherePositionUpdateCount = spherePositionUpdates.length; for( let i = 0; i < spherePositionUpdateCount; i++ ) { let updatedSphere = spherePositionUpdates[ i ]; let updatedSphereId = updatedSphere[ incomingMsgComponents.UPDATE_CLIENT_COORDS.SPHERE_ID ]; // check that this client is holding this sphere if( typeof spheresHeldByClient[ updatedSphereId ] === 'undefined' ) { continue; } let updatedSpherePosition = updatedSphere[ incomingMsgComponents.UPDATE_CLIENT_COORDS.SPHERE_POSITION ]; if( spheresHeldByClient[ updatedSphereId ] === true ) { // set the sphere position in state so new clients see it yield put( roomDataActions.setPositionForSphereInRoomRequestAction( updatedSpherePosition, updatedSphereId, action.roomName ) ); // add it to the list of checked updates checkedSpherePositionUpdates.push( updatedSphere ); } } // set a populated checked array back in the message if( checkedSpherePositionUpdates.length > 0 ) { msgData[ incomingMsgComponents.UPDATE_CLIENT_COORDS.SPHERES ] = checkedSpherePositionUpdates; } // or just remove an empty one (= no valid sphere updates) else { delete msgData[ incomingMsgComponents.UPDATE_CLIENT_COORDS.SPHERES ]; } } // don't record any client state change, just broadcast its new position to other clients let updatedCoordsMessage = makeWsBroadcastMessage( action[ incomingMsgComponents.ALL_MESSAGES.FROM ], messageConstants.OUTGOING_MESSAGE_TYPES.ROOM_CLIENT_COORDS_UPDATED, action[ incomingMsgComponents.ALL_MESSAGES.MSG ][ incomingMsgComponents.ALL_MESSAGES.DATA ] ); yield put( roomDataActions.publishMessageToRoomRequestAction( updatedCoordsMessage, action.roomName ) ); }; /******************************************************************************* * CREATE SPHERE AT POSITION IN ROOM *******************************************************************************/ const createSphereOfToneAtPosition = function* ( action ) { let { msgType, msgData } = getTypeAndDataForMessage( action ); let roomData = yield select( ( state ) => { return state.roomDataReducer; } ); let sphereState = roomData.rooms[ action.roomName ].content.spheres; // make sure there's sphere space in the room if( Object.keys( sphereState ).length >= sphereConstants.SPHERE_INFO.MAX_NUMBER_OF_SPHERES_PER_ROOM ) { denySphereAction( action.ws, null, // no sphere under discussion, so ... null, // ... no client holding it messageConstants.ERROR_TYPES.CREATE_SPHERE_UNAVAILABLE ); return; } // snag relevant data let newSphereId = uuid(); let tone = msgData[ incomingMsgComponents.CREATE_SPHERE_OF_TONE_AT_POSITION.TONE ]; let position = msgData[ incomingMsgComponents.CREATE_SPHERE_OF_TONE_AT_POSITION.POSITION ]; let clientId = action.ws.id; // create the sphere, save in sate yield put( roomDataActions.addSphereOfToneAtPositionInRoomRequestAction( newSphereId, tone, position, action.roomName ) ); // tell the client it created the sphere OK let createSuccessMessage = makeWsReplyMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.CREATE_SPHERE_SUCCESS, { [ outgoingMsgComponents.CREATE_SPHERE_SUCCESS.SPHERE_ID ]: newSphereId } ); sendWsMessageWithLogger( action.ws, createSuccessMessage, logger ); // broadcast 'sphere created' action let data = { [ outgoingMsgComponents.ROOM_SPHERE_CREATED.SPHERE_ID ]: newSphereId, [ outgoingMsgComponents.ROOM_SPHERE_CREATED.TONE ]: tone, [ outgoingMsgComponents.ROOM_SPHERE_CREATED.POSITION ]: position, [ outgoingMsgComponents.ROOM_SPHERE_CREATED.CLIENT_ID ]: clientId }; let sphereMsg = makeWsBroadcastMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.ROOM_SPHERE_CREATED, data ); yield put( roomDataActions.publishMessageToRoomRequestAction( sphereMsg, action.roomName ) ); }; /******************************************************************************* * GRAB SPHERE BY ID *******************************************************************************/ const grabSphere = function* ( action ) { let { msgType, msgData } = getTypeAndDataForMessage( action ); // get necessary state let roomState = yield select( ( state ) => { return state.roomDataReducer.rooms[ action.roomName ]; } ); // check the client has a spare hand let spheresHeldByClient = roomState.content.clients[ action.ws.id ].spheresHeld; // if it's already holding more than 1 ... if( Object.keys( spheresHeldByClient ).length > 1 ) { // it can't grab any more denySphereAction( action.ws, sphereId, null, // it would be invalid for this ws to grab it messageConstants.ERROR_TYPES.CLIENT_HOLDING_MAX_SPHERES ); return; } // snag relevant data let sphereId = msgData[ incomingMsgComponents.GRAB_SPHERE.SPHERE_ID ]; let sphereState = roomState.content.spheres; // does sphere exist? if( !sphereState[ sphereId ] ) { denySphereAction( action.ws, sphereId, null, // it would be invalid for this ws to grab it messageConstants.ERROR_TYPES.NON_EXISTENT_SPHERE ); return; }; // is it already held ... if( sphereState[ sphereId ].hold ) { let alreadyHeldErrorCode; let holderId = sphereState[ sphereId ].hold.clientId; // ... by this client? if( holderId === action.ws.id ) { alreadyHeldErrorCode = messageConstants.ERROR_TYPES.CLIENT_HOLDING_SPHERE; } // or another client? else { alreadyHeldErrorCode = messageConstants.ERROR_TYPES.SPHERE_ALREADY_HELD; } // either way, no go denySphereAction( action.ws, sphereId, holderId, // it would be invalid for this ws to grab it alreadyHeldErrorCode ); return; } // create a hold on this sphere for this client yield put( roomDataActions.createHoldOnSphereForClientInRoomRequestAction( sphereId, action.ws.id, action.roomName ) ); // TODO check the client got the hold? // tell the client it got a hold let grabSuccessMessage = makeWsReplyMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.GRAB_SPHERE_SUCCESS, { [ outgoingMsgComponents.GRAB_SPHERE_SUCCESS.SPHERE_ID ]: sphereId } ); sendWsMessageWithLogger( action.ws, grabSuccessMessage, logger ); // broadcast 'sphere grabbed' action let sphereGrabbedData = { [ outgoingMsgComponents.GRAB_SPHERE_SUCCESS.SPHERE_ID ]: sphereId, [ outgoingMsgComponents.GRAB_SPHERE_SUCCESS.CLIENT_ID ]: action.ws.id }; let sphereGrabbedMsg = makeWsBroadcastMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.ROOM_SPHERE_GRABBED, sphereGrabbedData ); yield put( roomDataActions.publishMessageToRoomRequestAction( sphereGrabbedMsg, action.roomName ) ); }; /******************************************************************************* * RELEASE SPHERE BY ID *******************************************************************************/ const releaseSphere = function* ( action ) { let { msgType, msgData } = getTypeAndDataForMessage( action ); // get necessary state let sphereState = yield getSphereStateForRoom( action.roomName ); // snag relevant data let sphereId = msgData[ incomingMsgComponents.RELEASE_SPHERE.SPHERE_ID ]; // does this sphere exist? if( !sphereState[ sphereId ] ) { denySphereAction( action.ws, sphereId, null, // it would be invalid for this ws to grab it messageConstants.ERROR_TYPES.NON_EXISTENT_SPHERE ); return; }; // is it held at all? if( !sphereState[ sphereId ].hold ) { denySphereAction( action.ws, sphereId, null, // it would be invalid for this ws to release it messageConstants.OUTGOING_MESSAGE_TYPES.RELEASE_SPHERE_INVALID ); return; } let holderId = sphereState[ sphereId ].hold.clientId; if( typeof holderId === 'undefined' ) { denySphereAction( action.ws, sphereId, null, messageConstants.ERROR_TYPES.SYSTEM_ERROR ); return; } // is it held by another client? if( holderId !== action.ws.id ) { denySphereAction( action.ws, sphereId, holderId, // denied because this other one's holding it messageConstants.OUTGOING_MESSAGE_TYPES.RELEASE_SPHERE_DENIED ); return; } // remove the hold on this sphere for this client yield put( roomDataActions.removeHoldOnSphereForClientInRoomRequestAction( sphereId, action.ws.id, action.roomName ) ); // tell the client it released the sphere OK let releaseSuccessMessage = makeWsReplyMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.RELEASE_SPHERE_SUCCESS, { [ outgoingMsgComponents.RELEASE_SPHERE_SUCCESS.SPHERE_ID ]: sphereId } ); sendWsMessageWithLogger( action.ws, releaseSuccessMessage, logger ); // broadcast to the room that the sphere's been released let outgoingMsgData = { [ outgoingMsgComponents.ROOM_SPHERE_RELEASED.SPHERE_ID ]: sphereId, [ outgoingMsgComponents.ROOM_SPHERE_RELEASED.CLIENT_ID ]: action.ws.id }; let sphereReleasedMessage = makeWsBroadcastMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.ROOM_SPHERE_RELEASED, outgoingMsgData ); yield put( roomDataActions.publishMessageToRoomRequestAction( sphereReleasedMessage, action.roomName ) ); }; /******************************************************************************* * SET SPHERE TONE BY ID *******************************************************************************/ const setSphereTone = function* ( action ) { let { msgType, msgData } = getTypeAndDataForMessage( action ); // get necessary state let sphereState = yield getSphereStateForRoom( action.roomName ); // snag relevant data let sphereId = msgData[ incomingMsgComponents.SET_SPHERE_TONE.SPHERE_ID ]; let tone = msgData[ incomingMsgComponents.SET_SPHERE_TONE.TONE ]; // does this sphere exist? if( !sphereState[ sphereId ] ) { denySphereAction( action.ws, sphereId, null, // it would be invalid for this ws to grab it messageConstants.ERROR_TYPES.NON_EXISTENT_SPHERE ); return; }; // is it held by another client? if( sphereState[ sphereId ].hold ) { let holderId = sphereState[ sphereId ].hold.clientId; if( typeof holderId === 'undefined' ) { denySphereAction( action.ws, sphereId, null, messageConstants.ERROR_TYPES.SYSTEM_ERROR ); return; } if( holderId !== action.ws.id ) { denySphereAction( action.ws, sphereId, holderId, // denied because this other one's holding it messageConstants.OUTGOING_MESSAGE_TYPES.SET_SPHERE_TONE_DENIED ); return; } } // set the tone for this sphere yield put( roomDataActions.setToneForSphereInRoomRequestAction( tone, sphereId, action.roomName ) ); // tell the client it set the tone OK let toneSuccessMessage = makeWsReplyMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.SET_SPHERE_TONE_SUCCESS, { [ outgoingMsgComponents.SET_SPHERE_TONE_SUCCESS.SPHERE_ID ]: sphereId } ); sendWsMessageWithLogger( action.ws, toneSuccessMessage, logger ); // broadcast the new tone to the room let outgoingMsgData = { [ outgoingMsgComponents.ROOM_SPHERE_TONE_SET.SPHERE_ID ]: sphereId, [ outgoingMsgComponents.ROOM_SPHERE_TONE_SET.TONE ]: tone, [ outgoingMsgComponents.ROOM_SPHERE_TONE_SET.CLIENT_ID ]: action.ws.id }; let sphereToneMsg = makeWsBroadcastMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.ROOM_SPHERE_TONE_SET, outgoingMsgData ); yield put( roomDataActions.publishMessageToRoomRequestAction( sphereToneMsg, action.roomName ) ); }; /******************************************************************************* * SET SPHERE CONNECTIONS BY ID *******************************************************************************/ const setSphereConnections = function* ( action ) { let { msgType, msgData } = getTypeAndDataForMessage( action ); // get necessary state let sphereState = yield getSphereStateForRoom( action.roomName ); // snag relevant data let sphereId = msgData[ incomingMsgComponents.SET_SPHERE_CONNECTIONS.SPHERE_ID ]; let connections = msgData[ incomingMsgComponents.SET_SPHERE_CONNECTIONS.CONNECTIONS ]; // make sure there aren't too many connections if( connections.length > sphereConstants.SPHERE_INFO.MAX_NUMBER_OF_CONNECTIONS_PER_SPHERE ) { denySphereAction( action.ws, sphereId, null, // it would be invalid for this ws to grab it messageConstants.ERROR_TYPES.TOO_MANY_SPHERE_CONNECTIONS ); return; } // does sphere exist? if( !sphereState[ sphereId ] ) { denySphereAction( action.ws, sphereId, null, // it would be invalid for this ws to grab it messageConstants.ERROR_TYPES.NON_EXISTENT_SPHERE ); return; }; // is it already held ... if( sphereState[ sphereId ].hold ) { let holderId = sphereState[ sphereId ].hold.clientId; // ... by another client? if( holderId !== action.ws.id ) { denySphereAction( action.ws, sphereId, holderId, // it would be invalid for this ws to grab it messageConstants.OUTGOING_MESSAGE_TYPES.SET_SPHERE_CONNECTIONS_DENIED ); return; } } // if it's a non-empty list of spheres to connect to if( connections.length > 0 ) { // make sure that ... let uniqueAvailableConnections = connections.filter( // spheres are not the same ( sphereIdIterator ) => { return sphereIdIterator !== sphereId; } ).filter( // spheres exist in room ( sphereIdIterator ) => { return Object.keys( sphereState ).indexOf( sphereIdIterator ) >= 0; } ); if( uniqueAvailableConnections.length == 0 ) { logger.trace( `setSphereConnections: no unique available connections requested` ); denySphereAction( action.ws, sphereId, null, // it would be invalid for this ws to set it messageConstants.OUTGOING_MESSAGE_TYPES.SET_SPHERE_CONNECTIONS_DENIED ); return; } } // set the list of connections for this sphere yield put( roomDataActions.setConnectionsForSphereInRoomRequestAction( connections, sphereId, action.roomName ) ); // tell the client it connected the spheres OK let connectSuccessMessage = makeWsReplyMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.SET_SPHERE_CONNECTIONS_SUCCESS, { [ outgoingMsgComponents.SET_SPHERE_CONNECTIONS_SUCCESS.SPHERE_ID ]: sphereId } ); sendWsMessageWithLogger( action.ws, connectSuccessMessage, logger ); // broadcast the new connections to the room let outgoingMsgData = { [ outgoingMsgComponents.ROOM_SPHERE_CONNECTIONS_SET.SPHERE_ID ]: sphereId, [ outgoingMsgComponents.ROOM_SPHERE_CONNECTIONS_SET.CONNECTIONS ]: connections, [ outgoingMsgComponents.ROOM_SPHERE_CONNECTIONS_SET.CLIENT_ID ]: action.ws.id }; let sphereConnectionsMsg = makeWsBroadcastMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.ROOM_SPHERE_CONNECTIONS_SET, outgoingMsgData ); yield put( roomDataActions.publishMessageToRoomRequestAction( sphereConnectionsMsg, action.roomName ) ); }; /******************************************************************************* * DELETE SPHERE BY ID *******************************************************************************/ const deleteSphere = function* ( action ) { let { msgType, msgData } = getTypeAndDataForMessage( action ); // get necessary state let sphereState = yield getSphereStateForRoom( action.roomName ); // snag relevant data let sphereId = msgData[ incomingMsgComponents.DELETE_SPHERE.SPHERE_ID ]; // does this sphere exist? if( !sphereState[ sphereId ] ) { denySphereAction( action.ws, sphereId, null, // it would be invalid for this ws to grab it messageConstants.ERROR_TYPES.NON_EXISTENT_SPHERE ); return; }; // is it held at all? if( sphereState[ sphereId ].hold ) { let holderId = sphereState[ sphereId ].hold.clientId; if( typeof holderId === 'undefined' ) { denySphereAction( action.ws, sphereId, null, messageConstants.ERROR_TYPES.SYSTEM_ERROR ); return; } // is it held by another client? if( holderId !== action.ws.id ) { denySphereAction( action.ws, sphereId, holderId, // denied because this other one's holding it messageConstants.OUTGOING_MESSAGE_TYPES.DELETE_SPHERE_DENIED ); return; } } // remove the hold on this sphere for this client yield put( roomDataActions.deleteSphereFromRoomRequestAction( sphereId, action.roomName ) ); // tell the client it deleted the sphere OK let deleteSuccessMessage = makeWsReplyMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.DELETE_SPHERE_SUCCESS, { [ outgoingMsgComponents.DELETE_SPHERE_SUCCESS.SPHERE_ID ]: sphereId } ); sendWsMessageWithLogger( action.ws, deleteSuccessMessage, logger ); // broadcast 'sphere deleted' action let outgoingMsgData = { [ outgoingMsgComponents.ROOM_SPHERE_DELETED.SPHERE_ID ]: sphereId, [ outgoingMsgComponents.ROOM_SPHERE_DELETED.CLIENT_ID ]: action.ws.id }; let sphereDeletedMsg = makeWsBroadcastMessage( action.ws.id, messageConstants.OUTGOING_MESSAGE_TYPES.ROOM_SPHERE_DELETED, outgoingMsgData ); yield put( roomDataActions.publishMessageToRoomRequestAction( sphereDeletedMsg, action.roomName ) ); }; /******************************************************************************* * STRIKE SPHERE BY ID *******************************************************************************/ const strikeSphere = function* ( action ) { let { msgType, msgData } = getTypeAndDataForMessage( action ); // get necessary state let sphereState = yield getSphereStateForRoom( action.roomName ); // snag relevant data let sphereId = msgData[ incomingMsgComponents.STRIKE_SPHERE.SPHERE_ID ]; let velocity = msgData[ incomingMsgComponents.STRIKE_SPHERE.VELOCITY ]; let fromId = action[ incomingMsgComponents.ALL_MESSAGES.FROM ]; // does sphere exist? if( !sphereState[ sphereId ] ) { // drop silently if no such sphere return; }; // no state change here, just broadcast 'sphere strike' action let outgoingMsgData = { [ outgoingMsgComponents.ROOM_SPHERE_STRUCK.SPHERE_ID ]: sphereId, [ outgoingMsgComponents.ROOM_SPHERE_STRUCK.VELOCITY ]: velocity, [ outgoingMsgComponents.ROOM_SPHERE_STRUCK.CLIENT_ID ]: fromId }; let sphereStruckMsg = makeWsBroadcastMessage( action.ws.id, messageConstants.OUTGOING_MESSAGE_TYPES.ROOM_SPHERE_STRUCK, outgoingMsgData ); yield put( roomDataActions.publishMessageToRoomRequestAction( sphereStruckMsg, action.roomName ) ); }; /******************************************************************************* * HELPER FUNCTIONS *******************************************************************************/ const getTypeAndDataForMessage = ( message ) => { return { msgType: message[ incomingMsgComponents.ALL_MESSAGES.TYPE ], msgData: message[ incomingMsgComponents.ALL_MESSAGES.MSG ][ incomingMsgComponents.ALL_MESSAGES.DATA ] }; }; const getSphereStateForRoom = function* ( roomName ) { let sphereState = yield select( ( state ) => { return state.roomDataReducer.rooms[ roomName ].content.spheres; } ); return sphereState; }; const denySphereAction = ( ws, sphereId, holderId, denialType ) => { let data = {}; if( sphereId ) { data[ outgoingMsgComponents.SPHERE_ACTION_DENIED.SPHERE_ID ] = sphereId; } if( holderId ) { data[ outgoingMsgComponents.SPHERE_ACTION_DENIED.HOLDER_ID ] = holderId; } let actionDeniedMessage = makeWsReplyMessage( config.serverId, denialType, data ); sendWsMessageWithLogger( ws, actionDeniedMessage, logger ); return; }; /******************************************************************************* * SAGA WATCHER FUNCTIONS * these takeEvery clauses match for INCOMING_MESSAGE_TYPES auto-dispatched by * messageHandler.handleWebsocketMessage() after message validation *******************************************************************************/ const watchExitRoomRequests = function* () { yield takeEvery( messageConstants.INCOMING_MESSAGE_TYPES.EXIT_ROOM, exitRoom ); }; const watchUpdateCoordsForClientRequests = function* () { yield takeEvery( messageConstants.INCOMING_MESSAGE_TYPES.UPDATE_CLIENT_COORDS, updateClientCoords ); }; const watchCreateSphereAtPositionRequests = function* () { yield takeEvery( messageConstants.INCOMING_MESSAGE_TYPES.CREATE_SPHERE_OF_TONE_AT_POSITION, createSphereOfToneAtPosition ); }; const watchGrabSphereRequests = function* () { yield takeEvery( messageConstants.INCOMING_MESSAGE_TYPES.GRAB_SPHERE, grabSphere ); }; const watchReleaseSphereRequests = function* () { yield takeEvery( messageConstants.INCOMING_MESSAGE_TYPES.RELEASE_SPHERE, releaseSphere ); }; const watchDeleteSphereRequests = function* () { yield takeEvery( messageConstants.INCOMING_MESSAGE_TYPES.DELETE_SPHERE, deleteSphere ); }; const watchSetSphereToneRequests = function* () { yield takeEvery( messageConstants.INCOMING_MESSAGE_TYPES.SET_SPHERE_TONE, setSphereTone ); }; const watchSetSphereConnectionsRequests = function* () { yield takeEvery( messageConstants.INCOMING_MESSAGE_TYPES.SET_SPHERE_CONNECTIONS, setSphereConnections ); }; const watchStrikeSphereRequests = function* () { yield takeEvery( messageConstants.INCOMING_MESSAGE_TYPES.STRIKE_SPHERE, strikeSphere ); }; export default { rootSaga: function* () { const sagas = [ watchExitRoomRequests, watchUpdateCoordsForClientRequests, watchCreateSphereAtPositionRequests, watchGrabSphereRequests, watchReleaseSphereRequests, watchDeleteSphereRequests, watchSetSphereToneRequests, watchSetSphereConnectionsRequests, watchStrikeSphereRequests ]; yield sagaUtils.spawnAutoRestartingSagas( sagas ); } }; ================================================ FILE: backend/src/messages/message-schema.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /******************************************************************************* * * this module implements a JSON schema for incoming messages. * it's specified directly as JS objects so we can: * * - have comments in the schema * - include other sections without needing separate JSON files * - distinguish between JSON schema property names (barewords) * and schema entry types ('quoted') * - dynamically construct the schema's "properties" list * from INCOMING_MESSAGE_TYPES constants * *******************************************************************************/ import { sphereConstants } from '../spheres'; import messageConstants from './message-constants'; // shorthand for dictionary of INCOMING_MESSAGE_TYPES constants let messageTypes = messageConstants.INCOMING_MESSAGE_TYPES; let messageComponents = messageConstants.INCOMING_MESSAGE_COMPONENTS; /******************************************************************************* * UTILITY FUNCTIONS *******************************************************************************/ // make 'type: "string"' schema definition entry using a messageTypes constant const makeStringTypeForMessageTypeName = ( name ) => { return { type: 'string', minLength: ( () => { return name.length; } )(), maxLength: ( () => { return name.length; } )() } }; /******************************************************************************* * INCOMING MESSAGE TYPE SCHEMA DEFINITIONS *******************************************************************************/ // exit current room const exitRoomMessageSchema = { type: 'object', properties: { [ messageComponents.ALL_MESSAGES.TYPE ]: ( () => { return makeStringTypeForMessageTypeName( messageTypes.EXIT_ROOM ); } )() }, required: [ messageComponents.ALL_MESSAGES.TYPE ] }; // "client position update" message const updateClientCoordsMessageSchema = { type: 'object', properties: { [ messageComponents.ALL_MESSAGES.TYPE ]: ( () => { return makeStringTypeForMessageTypeName( messageTypes.UPDATE_CLIENT_COORDS ); } )(), [ messageComponents.ALL_MESSAGES.DATA ]: { type: 'object', properties: { [ messageComponents.UPDATE_CLIENT_COORDS.HEAD ]: { '$ref': `#/definitions/${messageComponents.REFERENCES.COORDINATE_SET}` }, [ messageComponents.UPDATE_CLIENT_COORDS.LEFT ]: { '$ref': `#/definitions/${messageComponents.REFERENCES.COORDINATE_SET}` }, [ messageComponents.UPDATE_CLIENT_COORDS.RIGHT ]: { '$ref': `#/definitions/${messageComponents.REFERENCES.COORDINATE_SET}` }, // optional spheres, only used if client is holding spheres [ messageComponents.UPDATE_CLIENT_COORDS.SPHERES ]: { type: 'array', items: { type: 'object', properties: { [ messageComponents.UPDATE_CLIENT_COORDS.SPHERE_ID ]: { type: 'string', format: 'uuid' }, [ messageComponents.UPDATE_CLIENT_COORDS.SPHERE_POSITION ]: { '$ref': `#/definitions/${messageComponents.REFERENCES.COORDINATE}` } }, required: [ messageComponents.UPDATE_CLIENT_COORDS.SPHERE_ID, messageComponents.UPDATE_CLIENT_COORDS.SPHERE_POSITION ] } } }, required: [ messageComponents.UPDATE_CLIENT_COORDS.HEAD, messageComponents.UPDATE_CLIENT_COORDS.LEFT, messageComponents.UPDATE_CLIENT_COORDS.RIGHT ] } }, required: [ messageComponents.ALL_MESSAGES.TYPE, messageComponents.ALL_MESSAGES.DATA ] }; // 'create sphere at position' message const createSphereOfToneAtPositionMessageSchema = { type: 'object', properties: { [ messageComponents.ALL_MESSAGES.TYPE ]: ( () => makeStringTypeForMessageTypeName( messageTypes.CREATE_SPHERE_OF_TONE_AT_POSITION ) )(), [ messageComponents.ALL_MESSAGES.DATA ]: { type: 'object', properties: { [ messageComponents.CREATE_SPHERE_OF_TONE_AT_POSITION.TONE ]: { type: 'number', minimum: sphereConstants.SPHERE_INFO.LOWEST_TONE, maximum: sphereConstants.SPHERE_INFO.HIGHEST_TONE }, [ messageComponents.CREATE_SPHERE_OF_TONE_AT_POSITION.POSITION ]: { '$ref': `#/definitions/${messageComponents.REFERENCES.COORDINATE}` } }, required: [ messageComponents.CREATE_SPHERE_OF_TONE_AT_POSITION.TONE, messageComponents.CREATE_SPHERE_OF_TONE_AT_POSITION.POSITION ] } }, required: [ messageComponents.ALL_MESSAGES.TYPE, messageComponents.ALL_MESSAGES.DATA ] }; // 'grab sphere' const grabSphereMessageSchema = { type: 'object', properties: { [ messageComponents.ALL_MESSAGES.TYPE ]: ( () => makeStringTypeForMessageTypeName( messageTypes.GRAB_SPHERE ) )(), [ messageComponents.ALL_MESSAGES.DATA ]: { type: 'object', properties: { [ messageComponents.GRAB_SPHERE.SPHERE_ID ]: { type: 'string', format: 'uuid' } }, required: [ messageComponents.GRAB_SPHERE.SPHERE_ID ] } }, required: [ messageComponents.ALL_MESSAGES.TYPE, messageComponents.ALL_MESSAGES.DATA ] }; // 'release sphere' const releaseSphereMessageSchema = { type: 'object', properties: { [ messageComponents.ALL_MESSAGES.TYPE ]: ( () => makeStringTypeForMessageTypeName( messageTypes.RELEASE_SPHERE ) )(), [ messageComponents.ALL_MESSAGES.DATA ]: { type: 'object', properties: { [ messageComponents.RELEASE_SPHERE.SPHERE_ID ]: { type: 'string', format: 'uuid' } }, required: [ messageComponents.RELEASE_SPHERE.SPHERE_ID ] } }, required: [ messageComponents.ALL_MESSAGES.TYPE, messageComponents.ALL_MESSAGES.DATA ] }; // 'delete sphere' const deleteSphereMessageSchema = { type: 'object', properties: { [ messageComponents.ALL_MESSAGES.TYPE ]: ( () => makeStringTypeForMessageTypeName( messageTypes.DELETE_SPHERE ) )(), [ messageComponents.ALL_MESSAGES.DATA ]: { type: 'object', properties: { [ messageComponents.DELETE_SPHERE.SPHERE_ID ]: { type: 'string', format: 'uuid' } }, required: [ messageComponents.DELETE_SPHERE.SPHERE_ID ] } }, required: [ messageComponents.ALL_MESSAGES.TYPE, messageComponents.ALL_MESSAGES.DATA ] }; // 'strike sphere' const strikeSphereMessageSchema = { type: 'object', properties: { [ messageComponents.ALL_MESSAGES.TYPE ]: ( () => makeStringTypeForMessageTypeName( messageTypes.STRIKE_SPHERE ) )(), [ messageComponents.ALL_MESSAGES.DATA ]: { type: 'object', properties: { [ messageComponents.STRIKE_SPHERE.SPHERE_ID ]: { type: 'string', format: 'uuid' }, [ messageComponents.STRIKE_SPHERE.VELOCITY ]: { type: 'number', minimum: sphereConstants.SPHERE_INFO.MINIMUM_STRIKE_VELOCITY, maximum: sphereConstants.SPHERE_INFO.MAXIMUM_STRIKE_VELOCITY } }, required: [ messageComponents.STRIKE_SPHERE.SPHERE_ID, messageComponents.STRIKE_SPHERE.VELOCITY ] } }, required: [ messageComponents.ALL_MESSAGES.TYPE, messageComponents.ALL_MESSAGES.DATA ] }; // 'set sphere tone' const setSphereToneMessageSchema = { type: 'object', properties: { [ messageComponents.ALL_MESSAGES.TYPE ]: ( () => makeStringTypeForMessageTypeName( messageTypes.SET_SPHERE_TONE ) )(), [ messageComponents.ALL_MESSAGES.DATA ]: { type: 'object', properties: { [ messageComponents.SET_SPHERE_TONE.SPHERE_ID ]: { type: 'string', format: 'uuid' }, [ messageComponents.SET_SPHERE_TONE.TONE ]: { type: 'number', minimum: sphereConstants.SPHERE_INFO.LOWEST_TONE, maximum: sphereConstants.SPHERE_INFO.HIGHEST_TONE } }, required: [ messageComponents.SET_SPHERE_TONE.SPHERE_ID, messageComponents.SET_SPHERE_TONE.TONE ] } }, required: [ messageComponents.ALL_MESSAGES.TYPE, messageComponents.ALL_MESSAGES.DATA ] }; // 'set sphere connections' const setSphereConnectionsMessageSchema = { type: 'object', properties: { [ messageComponents.ALL_MESSAGES.TYPE ]: ( () => makeStringTypeForMessageTypeName( messageTypes.SET_SPHERE_CONNECTIONS ) )(), [ messageComponents.ALL_MESSAGES.DATA ]: { type: 'object', properties: { [ messageComponents.SET_SPHERE_CONNECTIONS.SPHERE_ID ]: { type: 'string', format: 'uuid' }, [ messageComponents.SET_SPHERE_CONNECTIONS.CONNECTIONS ]: { type: 'array', items: { type: 'string', format: 'uuid' } } }, required: [ messageComponents.SET_SPHERE_CONNECTIONS.SPHERE_ID, messageComponents.SET_SPHERE_CONNECTIONS.CONNECTIONS ] } }, required: [ messageComponents.ALL_MESSAGES.TYPE, messageComponents.ALL_MESSAGES.DATA ] }; /******************************************************************************* * MAPPING FROM INCOMING MESSAGE TYPES TO APPROPRIATE SCHEMA COMPONENTS ********************************************************************************/ const incomingMessageSchemaDefinitions = ( () => { let definitions = {}; let m = messageTypes; definitions[ m.EXIT_ROOM ] = exitRoomMessageSchema; definitions[ m.UPDATE_CLIENT_COORDS ] = updateClientCoordsMessageSchema; definitions[ m.CREATE_SPHERE_OF_TONE_AT_POSITION ] = createSphereOfToneAtPositionMessageSchema; definitions[ m.GRAB_SPHERE ] = grabSphereMessageSchema; definitions[ m.RELEASE_SPHERE ] = releaseSphereMessageSchema; definitions[ m.DELETE_SPHERE ] = deleteSphereMessageSchema; definitions[ m.STRIKE_SPHERE ] = strikeSphereMessageSchema; definitions[ m.SET_SPHERE_TONE ] = setSphereToneMessageSchema; definitions[ m.SET_SPHERE_CONNECTIONS ] = setSphereConnectionsMessageSchema; return definitions; })(); /******************************************************************************* * COMPONENT DEFINITIONS REFERRED TO IN MESSAGE TYPES, MUST BE INCLUDED IN SCHEMA ********************************************************************************/ const componentDefinitions = { // an x-y-z coordinate [ messageComponents.REFERENCES.COORDINATE ]: { type: 'object', properties: { [ messageComponents.REF_COORDINATE.X ]: { type: 'number' }, [ messageComponents.REF_COORDINATE.Y ]: { type: 'number' }, [ messageComponents.REF_COORDINATE.Z ]: { type: 'number' } }, required: [ messageComponents.REF_COORDINATE.X, messageComponents.REF_COORDINATE.Y, messageComponents.REF_COORDINATE.Z ] }, // a pair of coordinates for rotation/position [ messageComponents.REFERENCES.COORDINATE_SET ]: { type: 'object', properties: { [ messageComponents.REF_COORDINATE_SET.POSITION ]: { '$ref': `#/definitions/${messageComponents.REFERENCES.COORDINATE}` }, [ messageComponents.REF_COORDINATE_SET.ROTATION ]: { '$ref': `#/definitions/${messageComponents.REFERENCES.COORDINATE}` } }, required: [ messageComponents.REF_COORDINATE_SET.POSITION, messageComponents.REF_COORDINATE_SET.ROTATION ] } }; /******************************************************************************* * COMBINED LIST OF COMPONENT AND MESSAGE DEFINITIONS INCLUDED IN MAIN SCHEMA ********************************************************************************/ const messageSchemaDefinitions = Object.assign( componentDefinitions, incomingMessageSchemaDefinitions ); /******************************************************************************* * PROPERTIES USED IN MAIN SCHEMA ********************************************************************************/ /* * list of message types defined as properties in schema, * dynamically defined from the list of incoming message schema definitions * * comes out looking like this, as per JSON schema reqs: * * properties = { * room_client_position_update: { * '$ref': '#/definitions/room_client_position_update' * } * } */ const messageSchemaProperties = ( () => { let properties = {}; Object.keys( incomingMessageSchemaDefinitions ).forEach( ( messageTypeName ) => { properties[ messageTypeName ] = { '$ref': `#/definitions/${messageTypeName}` } } ); return properties; })(); /******************************************************************************* * MAIN SCHEMA OBJECT INCORPORATING DEFINITIONS AND PROPERTIES FROM ABOVE *******************************************************************************/ const messageSchema = { // these are type definitions shared by multiple objects definitions: messageSchemaDefinitions, // these are the various types properties: messageSchemaProperties }; export { messageSchema } ================================================ FILE: backend/src/messages/message-validator.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import ajv from 'ajv'; import { deserialize } from '../s11n'; import { messageSchema } from './message-schema'; import messageConstants from './message-constants'; const messageComponents = messageConstants.INCOMING_MESSAGE_COMPONENTS; /* * create a validator object for all incoming messages */ let ajvOptions = { allErrors: true, // log all validation errors, not just first removeAdditional: "all" // this modifies the original message(!) }; let validator = new ajv( ajvOptions ); let validate = validator.compile( messageSchema ); let messageTypesNotRequiringData = [ messageConstants.INCOMING_MESSAGE_TYPES.EXIT_ROOM ]; /* * validates messages received over the wire against the appropriate schema. * called from messageHandler:handleWebsocketMessageWithStore(). * * any kind of validation issue throws an exception, and messageHandler * drops the message. */ const validateMessage = ( wireMessage ) => { let deserializedMessage; try { deserializedMessage = deserialize( wireMessage ); } catch( error ) { throw error; } // only validate if there's a message type if( !deserializedMessage[ messageComponents.ALL_MESSAGES.TYPE ] ) { throw new Error( `incoming message has no '${messageComponents.ALL_MESSAGES.TYPE}' property` ); } // only validate if there's message data if( !deserializedMessage[ messageComponents.ALL_MESSAGES.DATA ] ) { // except for message types that don't require message data if( messageTypesNotRequiringData.indexOf( deserializedMessage[ messageComponents.ALL_MESSAGES.TYPE ] ) < 0 ) { throw new Error( `incoming message has no '${messageComponents.ALL_MESSAGES.DATA}' property` ); } } // only validate if it's an acceptable message type if( !isValidMessageType( deserializedMessage[ messageComponents.ALL_MESSAGES.TYPE ] ) ) { throw new Error( `incoming message has invalid type '${deserializedMessage[ messageComponents.ALL_MESSAGES.TYPE ]}'` ); } // run the validator return validateMessageOfType( deserializedMessage, deserializedMessage[ messageComponents.ALL_MESSAGES.TYPE ] ); }; /* * check the provided type is in the top level 'properties' of the messageSchema */ const isValidMessageType = ( type ) => { return Object.keys( messageSchema.properties ).indexOf( type ) > -1; }; /* * validator function for incoming messages */ const validateMessageOfType = ( message, type ) => { let objectToValidate = {}; objectToValidate[ type ] = message; let valid = validate( objectToValidate ) if( valid !== true ) { let errorMsgs = validate.errors.map( ( error ) => { return `${error.dataPath} ${error.message}`; } ); let errorMsg = errorMsgs.join( '; ' ); throw new Error( errorMsg ); } return message; }; export default { validateMessage }; ================================================ FILE: backend/src/pubsub/index.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import pubsubClient from './pubsub-client'; export { pubsubClient }; ================================================ FILE: backend/src/pubsub/pubsub-client.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import pubsub from '@google-cloud/pubsub'; import config from '../config'; import { logger } from '../logger'; let client = ( ( conf ) => { if( !client ) { if( process.env.PUBSUB_EMULATOR_HOST ) { logger.info( `using PubSub emulator running at ${process.env.PUBSUB_EMULATOR_HOST}` ); } else { logger.info( `using live PubSub on GCP` ); } client = pubsub( conf ); } return client; })( config ); const createPubsubTopic = function* ( topicName ) { return yield client.createTopic( topicName ); }; const getPubsubTopic = function* ( topicName ) { return yield client.topic( topicName ); }; export default { createPubsubTopic, getPubsubTopic }; ================================================ FILE: backend/src/rooms/index.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import roomDataActions from './room-data-actions'; import roomDataConstants from './room-data-constants'; import roomDataReducer from './room-data-reducer'; import roomNames from './room-names'; import roomSagas from './room-sagas'; import roomStateActions from './room-state-actions'; import roomStateConstants from './room-state-constants'; import roomStateReducer from './room-state-reducer'; import roomStateUtils from './room-state-utils'; export { roomDataActions, roomDataConstants, roomDataReducer, roomNames, roomSagas, roomStateActions, roomStateConstants, roomStateReducer, roomStateUtils }; ================================================ FILE: backend/src/rooms/room-data-actions.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import constants from './room-data-constants'; /******************************************************************************* * ADD/REMOVE CLIENTS TO/FROM ROOMS *******************************************************************************/ const addWebsocketToQueueForRoomRequestAction = ( wsId, roomName ) => { let type = constants.ACTION_TYPES.ADD_WEBSOCKET_TO_QUEUE_FOR_ROOM; return { type, wsId, roomName }; }; const removeWebsocketFromQueueForRoomRequestAction = ( wsId, roomName ) => { let type = constants.ACTION_TYPES.REMOVE_WEBSOCKET_FROM_QUEUE_FOR_ROOM; return { type, wsId, roomName }; }; const addLocalClientToRoomRequestAction = ( clientId, clientHeadsetType, roomName ) => { let type = constants.ACTION_TYPES.ADD_LOCAL_CLIENT_TO_ROOM; return { type, clientId, clientHeadsetType, roomName }; }; const removeLocalClientFromRoomRequestAction = ( clientId, roomName ) => { let type = constants.ACTION_TYPES.REMOVE_LOCAL_CLIENT_FROM_ROOM; return { type, clientId, roomName }; }; /******************************************************************************* * ROOM SETUP/MAINTAIN/TEARDOWN *******************************************************************************/ const setLastHeartbeatCountForRoomRequestAction = ( count, roomName ) => { let type = constants.ACTION_TYPES.UPDATE_LAST_HEARTBEAT_FOR_ROOM; return { type, count, roomName }; }; const setHeartbeatTaskForRoomRequestAction = ( heartbeatTask, roomName ) => { let type = constants.ACTION_TYPES.SET_HEARTBEAT_TASK_FOR_ROOM; return { type, heartbeatTask, roomName }; }; const removeHeartbeatTaskForRoomRequestAction = ( roomName ) => { let type = constants.ACTION_TYPES.REMOVE_HEARTBEAT_TASK_FOR_ROOM; return { type, roomName }; }; const cancelHeartbeatTaskForRoomRequestAction = ( roomName ) => { let type = constants.ACTION_TYPES.CANCEL_HEARTBEAT_TASK_FOR_ROOM; return { type, roomName }; }; const checkForEmptyRoomRequestAction = ( roomName ) => { let type = constants.ACTION_TYPES.CHECK_FOR_EMPTY_ROOM; return { type, roomName }; }; /******************************************************************************* * SPHERE STATE ACTIONS *******************************************************************************/ const addSphereOfToneAtPositionInRoomRequestAction = ( newSphereId, tone, position, roomName ) => { let type = constants.ACTION_TYPES.ADD_SPHERE_OF_TONE_AT_POSITION_IN_ROOM; return { type, newSphereId, tone, position, roomName }; }; const createHoldOnSphereForClientInRoomRequestAction = ( sphereId, clientId, roomName ) => { let type = constants.ACTION_TYPES.CREATE_HOLD_ON_SPHERE_FOR_CLIENT_IN_ROOM; return { type, sphereId, clientId, roomName }; }; const setHoldTimeoutTaskForSphereInRoomRequestAction = ( timeoutTask, sphereId, roomName ) => { let type = constants.ACTION_TYPES.SET_HOLD_TIMEOUT_TASK_FOR_SPHERE_IN_ROOM; return { type, timeoutTask, sphereId, roomName }; }; const deleteHoldTimeoutTaskForSphereInRoomRequestAction = ( sphereId, roomName ) => { let type = constants.ACTION_TYPES.DELETE_HOLD_TIMEOUT_TASK_FOR_SPHERE_IN_ROOM; return { type, sphereId, roomName }; }; const setPositionForSphereInRoomRequestAction = ( position, sphereId, roomName ) => { let type = constants.ACTION_TYPES.SET_POSITION_FOR_SPHERE_IN_ROOM; return { type, position, sphereId, roomName }; }; const setToneForSphereInRoomRequestAction = ( tone, sphereId, roomName ) => { let type = constants.ACTION_TYPES.SET_TONE_FOR_SPHERE_IN_ROOM; return { type, tone, sphereId, roomName }; }; const setConnectionsForSphereInRoomRequestAction = ( connections, sphereId, roomName ) => { let type = constants.ACTION_TYPES.SET_CONNECTIONS_FOR_SPHERE_IN_ROOM; return { type, connections, sphereId, roomName }; }; const removeHoldOnSphereForClientInRoomRequestAction = ( sphereId, clientId, roomName ) => { let type = constants.ACTION_TYPES.REMOVE_HOLD_ON_SPHERE_FOR_CLIENT_IN_ROOM; return { type, sphereId, clientId, roomName }; }; const deleteSphereFromRoomRequestAction = ( sphereId, roomName ) => { let type = constants.ACTION_TYPES.DELETE_SPHERE_FROM_ROOM; return { type, sphereId, roomName }; }; /******************************************************************************* * PUBLISH MESSAGE *******************************************************************************/ const publishMessageToRoomRequestAction = ( message, roomName ) => { let type = constants.ACTION_TYPES.PUBLISH_MESSAGE_TO_ROOM; return { type, message, roomName }; }; export default { // add/remove clients to/from rooms addWebsocketToQueueForRoomRequestAction, removeWebsocketFromQueueForRoomRequestAction, addLocalClientToRoomRequestAction, removeLocalClientFromRoomRequestAction, // room setup/maintain/teardown setLastHeartbeatCountForRoomRequestAction, setHeartbeatTaskForRoomRequestAction, removeHeartbeatTaskForRoomRequestAction, cancelHeartbeatTaskForRoomRequestAction, checkForEmptyRoomRequestAction, // sphere state actions addSphereOfToneAtPositionInRoomRequestAction, createHoldOnSphereForClientInRoomRequestAction, setHoldTimeoutTaskForSphereInRoomRequestAction, deleteHoldTimeoutTaskForSphereInRoomRequestAction, setPositionForSphereInRoomRequestAction, setToneForSphereInRoomRequestAction, setConnectionsForSphereInRoomRequestAction, removeHoldOnSphereForClientInRoomRequestAction, deleteSphereFromRoomRequestAction, // publish message saga action publishMessageToRoomRequestAction }; ================================================ FILE: backend/src/rooms/room-data-constants.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { HEADSET_TYPES } from '../messages/message-constants'; const ACTION_TYPES = { ADD_WEBSOCKET_TO_QUEUE_FOR_ROOM: 'add_websocket_to_queue_for_room', REMOVE_WEBSOCKET_FROM_QUEUE_FOR_ROOM: 'remove_websocket_from_queue_for_room', ADD_LOCAL_CLIENT_TO_ROOM: 'add_local_client_to_room', REMOVE_LOCAL_CLIENT_FROM_ROOM: 'remove_local_client_from_room', // room setup/teardown UPDATE_LAST_HEARTBEAT_FOR_ROOM: 'update_last_heartbeat_for_room', SET_HEARTBEAT_TASK_FOR_ROOM: 'set_heartbeat_task_for_room', REMOVE_HEARTBEAT_TASK_FOR_ROOM: 'remove_heartbeat_task_for_room', CANCEL_HEARTBEAT_TASK_FOR_ROOM: 'cancel_heartbeat_task_for_room', CHECK_FOR_EMPTY_ROOM: 'check_for_empty_room', // client state actions SET_COORDS_FOR_CLIENT_IN_ROOM: 'set_coords_for_client_in_room', REMOVE_CLIENT_FROM_ROOM: 'remove_client_from_room', // sphere state actions ADD_SPHERE_OF_TONE_AT_POSITION_IN_ROOM: 'add_sphere_of_tone_at_position_in_room', SET_POSITION_FOR_SPHERE_IN_ROOM: 'set_position_for_sphere_in_room', SET_TONE_FOR_SPHERE_IN_ROOM: 'set_tone_for_sphere_in_room', SET_CONNECTIONS_FOR_SPHERE_IN_ROOM: 'set_connections_for_sphere_in_room', CREATE_HOLD_ON_SPHERE_FOR_CLIENT_IN_ROOM: 'create_hold_on_sphere_for_client_in_room', SET_HOLD_TIMEOUT_TASK_FOR_SPHERE_IN_ROOM: 'set_hold_timeout_task_for_sphere_in_room', DELETE_HOLD_TIMEOUT_TASK_FOR_SPHERE_IN_ROOM: 'delete_hold_timeout_task_for_sphere_in_room', REMOVE_HOLD_ON_SPHERE_FOR_CLIENT_IN_ROOM: 'remove_hold_on_sphere_for_client_in_room', DELETE_SPHERE_FROM_ROOM: 'delete_sphere_from_room', // publish to clients in room PUBLISH_MESSAGE_TO_ROOM: 'publish_message_to_room' }; const CLIENT_INFO = { CLIENT_TYPE_LOCAL: 'client_type_local', CLIENT_TYPE_REMOTE: 'client_type_remote', CLIENT_INACTIVITY_TIMEOUTS_IN_MS: { [ HEADSET_TYPES.HEADSET_TYPE_3DOF ]: 120000, // 2m [ HEADSET_TYPES.HEADSET_TYPE_6DOF ]: 120000, // 2m [ HEADSET_TYPES.HEADSET_TYPE_VIEWER ]: 120000, // 2m }, CLIENT_INACTIVITY_TIMEOUT_CHECKS_IN_MS: { [ HEADSET_TYPES.HEADSET_TYPE_3DOF ]: 30000, // 30s [ HEADSET_TYPES.HEADSET_TYPE_6DOF ]: 30000, // 30s [ HEADSET_TYPES.HEADSET_TYPE_VIEWER ]: 30000, // 30s } }; const ERROR_TYPES = { ROOM_TIMEOUT: 'room_timeout', JOINING_ROOM: 'error_joining_room', ROOM_FULL: 'error_room_full', WAITING_FOR_STATE: 'waiting_for_state' }; const ROOM_INFO = { MINIMUM_SOUNDBANK_NUMBER: 0, MAXIMUM_SOUNDBANK_NUMBER: 2 }; const SPHERE_INFO = { SPHERE_HOLD_TIMEOUT_IN_MS: 10000, SPHERE_HOLD_TIMEOUT_CHECK_IN_MS: 2500 }; export default { ACTION_TYPES, CLIENT_INFO, ERROR_TYPES, ROOM_INFO, SPHERE_INFO }; ================================================ FILE: backend/src/rooms/room-data-reducer.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import redux from 'redux'; import uuid from 'uuid'; import sizeof from 'object-sizeof'; import util from 'util'; import { initStateObject, createFilteredActionHandler } from '../utils/reducer-utils'; import { logger } from '../logger'; import messageConstants from '../messages/message-constants'; const HT_3DOF = messageConstants.HEADSET_TYPES.HEADSET_TYPE_3DOF; const HT_6DOF = messageConstants.HEADSET_TYPES.HEADSET_TYPE_6DOF; const HT_VIEW = messageConstants.HEADSET_TYPES.HEADSET_TYPE_VIEWER; const CONNECTIONS_LABEL = messageConstants.OUTGOING_MESSAGE_COMPONENTS.ROOM_STATUS_INFO.CONNECTIONS; const POSITION_LABEL = messageConstants.OUTGOING_MESSAGE_COMPONENTS.ROOM_STATUS_INFO.POSITION; const TONE_LABEL = messageConstants.OUTGOING_MESSAGE_COMPONENTS.ROOM_STATUS_INFO.TONE; import { makeSphereOfToneAtPosition, sphereConstants, trees } from '../spheres'; import config from '../config'; import roomDataConstants from './room-data-constants'; import roomStateConstants from './room-state-constants'; import roomNames from './room-names'; /******************************************************************************* * HELPER FUNCTIONS *******************************************************************************/ const createEmptyRoom = () => { return { heartbeatTask: null, lastHeartbeat: { count: 0, timestamp: 0 }, setupComplete: false, waitingForStateStatus: false, checkingForEmptyRoomStatus: false, soundbank: null, spheres: {}, websockets: {}, clients: {} }; }; const createNewRoom = () => { let emptyRoom = createEmptyRoom(); emptyRoom.soundbank = getRandomInt( roomDataConstants.ROOM_INFO.MINIMUM_SOUNDBANK_NUMBER, roomDataConstants.ROOM_INFO.MAXIMUM_SOUNDBANK_NUMBER ); emptyRoom.spheres = generateSpheresForRoom( emptyRoom.soundbank ); return emptyRoom; }; const generateSpheresForRoom = ( soundbank ) => { const roomSpheres = {} function dist(a, b){ return Math.sqrt(Math.pow(a.x - b.x, 2) + // Math.pow(a.y - b.y, 2) + Math.pow(a.z - b.z, 2)) } function randomPos(){ const rad = 2.5 const pos = { x : Math.random() * rad - rad/2, y : Math.random() * 0.7 + 0.9, z : Math.random() * rad - rad/2 } // check that the position is not too close to any other sphere let closestDist = Infinity for (let id in roomSpheres){ const roomPos = roomSpheres[id][POSITION_LABEL] closestDist = Math.min(dist(pos, roomPos), closestDist) } //only return the position if its greater than the thresh if (closestDist > 0.5){ return pos } else { //otherwise generate a new one return randomPos() } } for (let i = 0; i < 10; i++){ roomSpheres[uuid()] = { [TONE_LABEL] : getRandomInt(sphereConstants.SPHERE_INFO.LOWEST_TONE, sphereConstants.SPHERE_INFO.HIGHEST_TONE), [POSITION_LABEL] : randomPos() } } // return spheres return roomSpheres; }; const getRandomInt = ( min, max ) => { let intMin = Math.ceil( min ); let intMax = Math.floor( max ); return Math.floor( Math.random() * ( intMax - intMin + 1 ) ) + intMin; }; const getZeroCoordsForNewClient = () => { return { head: { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 } }, left: { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 } }, right: { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 } } }; }; /******************************************************************************* * GENERATE INITIAL EMPTY STATE FOR ALL ROOMS *******************************************************************************/ // dictionary of empty room data objects for all names const initialState = ( () => { let createFunction = () => { return { queue: [], tasks: {} }; }; return { rooms: initStateObject( roomNames, createFunction, 'room data objects', logger ), avails: ( () => { let obj = {}; Object.values( messageConstants.HEADSET_TYPES ).forEach( ( headsetType ) => { obj[ headsetType ] = {}; } ); return obj; } )() }; })(); /******************************************************************************* * ROOM INFO/STATUS REDUCER FUNCTIONS *******************************************************************************/ // create a new room state including spheres // action: { roomName } const initRoomContent = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } state.rooms[ action.roomName ].content = createNewRoom(); logger.trace( `created default content for new room ${action.roomName}` ); return state; }; // action: { wsId, roomName } const addWebsocketToQueueForRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } if( state.rooms[ action.roomName ].queue.indexOf( action.wsId ) >= 0 ) { return state; } state.rooms[ action.roomName ].queue.push( action.wsId ); logger.trace( `[+] ${state.rooms[ action.roomName ].queue.length} websockets in queue for room ${action.roomName}` ); return state; }; // action: { wsId, roomName } const removeWebsocketFromQueueForRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } state.rooms[ action.roomName ].queue = state.rooms[ action.roomName ].queue.filter( ( clientId ) => { return clientId != action.wsId; } ); logger.trace( `[-] ${state.rooms[ action.roomName ].queue.length} websockets in queue for room ${action.roomName}` ); return state; }; /******************************************************************************* * CLIENT REDUCER FUNCTIONS *******************************************************************************/ // action: { clientId, roomName } const addLocalClientToRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } let roomClients = state.rooms[ action.roomName ].content.clients; if( typeof roomClients[ action.clientId ] !== 'undefined' ) { return state; } roomClients[ action.clientId ] = { headsetType: action.clientHeadsetType, coords: getZeroCoordsForNewClient(), spheresHeld: {} } logger.trace( `[+] there are now ${Object.keys( roomClients ).length} total clients in room '${action.roomName}'` ); return updateClientCountsForRoom( state, action ); }; // action: { clientId, roomName } const removeLocalClientFromRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } state.rooms[ action.roomName ].content.clients[ action.clientId ] = null; delete state.rooms[ action.roomName ].content.clients[ action.clientId ]; let roomClients = state.rooms[ action.roomName ].content.clients; logger.trace( `[-] there are now ${Object.keys( roomClients ).length} total clients in room '${action.roomName}'` ); return updateClientCountsForRoom( state, action ); }; /* * for a room to be available to 6dof: * * 6dof must be less than 6dofThreshold * 3dof + 6dof must be less than 3dofThreshold + 6dofThreshold * 3dof + 6dof + viewers must be less than maxClientsPerRoom * * for a room to be available to 3dof: * * 3dof must be less than 3dofThreshold * 3dof + 6dof must be less than 3dofThreshold + 6dofThreshold * 3dof + 6dof + viewers must be less than maxClientsPerRoom * * for a room to be available to viewer: * * 3dof + 6dof + viewers must be less than maxClientsPerRoom */ const updateClientCountsForRoom = ( state, action ) => { // this gives { '3dof': 2, '6dof': 1, viewer: 4 } let clientCounts = Object.values( state.rooms[ action.roomName ].content.clients ).reduce( ( acc, client ) => { acc[ client.headsetType ] = acc[ client.headsetType ] + 1; return acc; }, ( () => { let obj = {}; Object.values( messageConstants.HEADSET_TYPES ).forEach( ( headsetType ) => { obj[ headsetType ] = 0; } ); return obj; } )() ); // this gives { '3dof': 2 + '6dof': 1 + viewer: 4 } = totalClients: 7 let totalClients = Object.values( clientCounts ).reduce( ( acc, count ) => { return acc + count; }, 0 ); // if the room's full, it's not available at all; // if it's empty, it doesn't go in the availability lists, // as it'll be found from the list of INIT-state rooms // in roomStateReducer.roomsByState if( totalClients >= config.maxClientsPerRoom || totalClients === 0) { delete state.avails[ HT_6DOF ][ action.roomName ]; delete state.avails[ HT_3DOF ][ action.roomName ]; delete state.avails[ HT_VIEW ][ action.roomName ]; return state; } else { state.avails[ HT_VIEW ][ action.roomName ] = true; } let num6dof = clientCounts[ HT_6DOF ]; let num3dof = clientCounts[ HT_3DOF ]; let numViewers = clientCounts[ HT_VIEW ]; let threshold6dof = config.headsetRules[ HT_6DOF ].threshold; let threshold3dof = config.headsetRules[ HT_3DOF ].threshold; let thresholdViewer = config.headsetRules[ HT_VIEW ].threshold; let totalStrikers = num6dof + num3dof; let strikerThreshold = threshold6dof + threshold3dof; if( num6dof < threshold6dof && totalStrikers < strikerThreshold ) { state.avails[ HT_6DOF ][ action.roomName ] = true; } else { delete state.avails[ HT_6DOF ][ action.roomName ]; } if( num3dof < threshold3dof && totalStrikers < strikerThreshold ) { state.avails[ HT_3DOF ][ action.roomName ] = true; } else { delete state.avails[ HT_3DOF ][ action.roomName ]; } return state; }; // action: { count, roomName } const setLastHeartbeatCountForRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } let newHeartbeat = { count: action.count, timestamp: Date.now() }; state.rooms[ action.roomName ].content.lastHeartbeat = newHeartbeat; return state; }; // action: { heartbeatTask, roomName } const setHeartbeatTaskForRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } state.rooms[ action.roomName ].tasks.heartbeat = action.heartbeatTask; return state; }; // action: { roomName } const removeHeartbeatTaskForRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } state.rooms[ action.roomName ].tasks.heartbeat = null; delete state.rooms[ action.roomName ].tasks.heartbeat; return state; }; /******************************************************************************* * SPHERE REDUCER FUNCTIONS *******************************************************************************/ // - called via redux-action from message-sagas:createSphereAtPositionInRoom // action: { newSphereId, position, tone, roomName } const addSphereOfToneAtPositionInRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } try { state.rooms[ action.roomName ].content.spheres[ action.newSphereId ] = makeSphereOfToneAtPosition( action.tone, action.position ); return state; } catch( error ) { logger.warn( `caught error trying to create new sphere: ${error.message}` ); } }; // - called via redux-action from message-sagas:updateClientCoords // - spheres in state are sent direct to clients, so use const labels // - action properties are fixed rather than using messageConstants // - sphere.position is a coordinates object as per message schema // action: { position, sphereId, roomName } const setPositionForSphereInRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } if( typeof state.rooms[ action.roomName ].content.spheres[ action.sphereId ] === 'undefined' ) { return state; } const positionLabel = messageConstants.INCOMING_MESSAGE_COMPONENTS.UPDATE_CLIENT_COORDS.SPHERE_POSITION; state.rooms[ action.roomName ].content.spheres[ action.sphereId ][ positionLabel ] = action.position; if( state.rooms[ action.roomName ].content.spheres[ action.sphereId ].hold ) { state.rooms[ action.roomName ].content.spheres[ action.sphereId ].hold.ts = Date.now(); } return state; }; // - called via redux-action from message-sagas:setSphereConnections // - spheres in state are sent direct to clients, so use const labels // - action properties are fixed rather than using messageConstants // action: { connections, sphereId, roomName } const setConnectionsForSphereInRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } if( typeof state.rooms[ action.roomName ].content.spheres[ action.sphereId ] === 'undefined' ) { return state; } const connectionsLabel = messageConstants.INCOMING_MESSAGE_COMPONENTS.SET_SPHERE_CONNECTIONS.CONNECTIONS; state.rooms[ action.roomName ].content.spheres[ action.sphereId ][ connectionsLabel ] = action.connections; if( state.rooms[ action.roomName ].content.spheres[ action.sphereId ].hold ) { state.rooms[ action.roomName ].content.spheres[ action.sphereId ].hold.ts = Date.now(); } return state; }; // - called via redux-action from message-sagas:setSphereTone // - spheres in state are sent direct to clients, so use const labels // - action properties are fixed rather than using messageConstants // action: { tone, sphereId, roomName } const setToneForSphereInRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } if( typeof state.rooms[ action.roomName ].content.spheres[ action.sphereId ] === 'undefined' ) { return state; } const toneLabel = messageConstants.INCOMING_MESSAGE_COMPONENTS.SET_SPHERE_TONE.TONE; state.rooms[ action.roomName ].content.spheres[ action.sphereId ][ toneLabel ] = action.tone; if( state.rooms[ action.roomName ].content.spheres[ action.sphereId ].hold ) { state.rooms[ action.roomName ].content.spheres[ action.sphereId ].hold.ts = Date.now(); } return state; }; // - called via redux-action from message-sagas:grabSphere // action: { sphereId, clientId, roomName } const createHoldOnSphereForClientInRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } if( typeof state.rooms[ action.roomName ].content.spheres[ action.sphereId ] === 'undefined' ) { return state; } if( typeof state.rooms[ action.roomName ].content.clients[ action.clientId ] === 'undefined' ) { return state; } // set a hold property on the sphere state.rooms[ action.roomName ].content.spheres[ action.sphereId ].hold = { clientId: action.clientId, ts: Date.now() }; // mark the client as holding this sphere state.rooms[ action.roomName ].content.clients[ action.clientId ].spheresHeld[ action.sphereId ] = true; return state; }; // - called from room-sagas:startSphereHoldTimeout // - triggered when a sphere hold is created // action: { timeoutTask, sphereId, roomName } const setHoldTimeoutTaskForSphereInRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } if( typeof state.rooms[ action.roomName ].content.spheres[ action.sphereId ] === 'undefined' ) { return state; } if( typeof state.rooms[ action.roomName ].content.spheres[ action.sphereId ].hold === 'undefined' ) { return state; } state.rooms[ action.roomName ].content.spheres[ action.sphereId ].hold.timeoutTask = action.timeoutTask; return state; }; // action: { sphereId, roomName } const deleteHoldTimeoutTaskForSphereInRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } if( typeof state.rooms[ action.roomName ].content.spheres[ action.sphereId ] === 'undefined' ) { return state; } if( typeof state.rooms[ action.roomName ].content.spheres[ action.sphereId ].hold === 'undefined' ) { return state; } state.rooms[ action.roomName ].content.spheres[ action.sphereId ].hold.timeoutTask = null; delete state.rooms[ action.roomName ].content.spheres[ action.sphereId ].hold.timeoutTask; return state; }; // - called via redux-action from message-sagas:releaseSphere // action: { sphereId, clientId, roomName } const removeHoldOnSphereForClientInRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } if( typeof state.rooms[ action.roomName ].content.spheres[ action.sphereId ] === 'undefined' ) { return state; } if( typeof state.rooms[ action.roomName ].content.clients[ action.clientId ] === 'undefined' ) { return state; } if( state.rooms[ action.roomName ].content.spheres[ action.sphereId ].hold.clientId == action.clientId ) { // release the sphere hold so other clients can grab it state.rooms[ action.roomName ].content.spheres[ action.sphereId ].hold = null; delete state.rooms[ action.roomName ].content.spheres[ action.sphereId ].hold; // disassociate the sphere from the client so it can't update it any more state.rooms[ action.roomName ].content.clients[ action.clientId ].spheresHeld[ action.sphereId ] = null; delete state.rooms[ action.roomName ].content.clients[ action.clientId ].spheresHeld[ action.sphereId ]; } return state; }; // - called via redux-action from message-sagas:deleteSphere // action: { sphereId, roomName } const deleteSphereFromRoom = ( state, action ) => { if( typeof action.roomName === 'undefined' ) { return state; } if( typeof state.rooms[ action.roomName ].content.spheres[ action.sphereId ] === 'undefined' ) { return state; } state.rooms[ action.roomName ].content.spheres[ action.sphereId ] = null; delete state.rooms[ action.roomName ].content.spheres[ action.sphereId ]; return state; }; const actionHandlers = { // room setup/teardown [ roomStateConstants.EVENT_TYPES.INIT_ROOM_CONTENT ]: initRoomContent, [ roomDataConstants.ACTION_TYPES.ADD_WEBSOCKET_TO_QUEUE_FOR_ROOM ]: addWebsocketToQueueForRoom, [ roomDataConstants.ACTION_TYPES.REMOVE_WEBSOCKET_FROM_QUEUE_FOR_ROOM ]: removeWebsocketFromQueueForRoom, [ roomDataConstants.ACTION_TYPES.ADD_LOCAL_CLIENT_TO_ROOM ]: addLocalClientToRoom, [ roomDataConstants.ACTION_TYPES.REMOVE_LOCAL_CLIENT_FROM_ROOM ]: removeLocalClientFromRoom, [ roomDataConstants.ACTION_TYPES.UPDATE_LAST_HEARTBEAT_FOR_ROOM ]: setLastHeartbeatCountForRoom, [ roomDataConstants.ACTION_TYPES.SET_HEARTBEAT_TASK_FOR_ROOM ]: setHeartbeatTaskForRoom, [ roomDataConstants.ACTION_TYPES.REMOVE_HEARTBEAT_TASK_FOR_ROOM ]: removeHeartbeatTaskForRoom, // sphere state actions [ roomDataConstants.ACTION_TYPES.ADD_SPHERE_OF_TONE_AT_POSITION_IN_ROOM ]: addSphereOfToneAtPositionInRoom, [ roomDataConstants.ACTION_TYPES.SET_POSITION_FOR_SPHERE_IN_ROOM ]: setPositionForSphereInRoom, [ roomDataConstants.ACTION_TYPES.SET_TONE_FOR_SPHERE_IN_ROOM ]: setToneForSphereInRoom, [ roomDataConstants.ACTION_TYPES.SET_CONNECTIONS_FOR_SPHERE_IN_ROOM ]: setConnectionsForSphereInRoom, [ roomDataConstants.ACTION_TYPES.CREATE_HOLD_ON_SPHERE_FOR_CLIENT_IN_ROOM ]: createHoldOnSphereForClientInRoom, [ roomDataConstants.ACTION_TYPES.SET_HOLD_TIMEOUT_TASK_FOR_SPHERE_IN_ROOM ]: setHoldTimeoutTaskForSphereInRoom, [ roomDataConstants.ACTION_TYPES.DELETE_HOLD_TIMEOUT_TASK_FOR_SPHERE_IN_ROOM ]: deleteHoldTimeoutTaskForSphereInRoom, [ roomDataConstants.ACTION_TYPES.REMOVE_HOLD_ON_SPHERE_FOR_CLIENT_IN_ROOM ]: removeHoldOnSphereForClientInRoom, [ roomDataConstants.ACTION_TYPES.DELETE_SPHERE_FROM_ROOM ]: deleteSphereFromRoom }; export default createFilteredActionHandler( actionHandlers, initialState ); ================================================ FILE: backend/src/rooms/room-names.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const roomNames = ["aaag","aace","aadx","aafo","aafx","aalu","aamd","aamm","aany","aaoc","aarm","aasm","aavz","aawl","aaxz","aayr","aayz","abbo","abip","abji","ablh","ablk","ablo","abod","abrn","abrw","abux","abvo","abvt","abvv","abyi","accr","acdc","acdr","aceq","acgv","achk","acjo","acnh","acod","acpe","acso","actg","acwb","acwy","acxg","adao","adfx","adga","adgo","adgw","adjg","adqo","adtg","advp","advy","adyp","aebs","aech","aefp","aehz","aejm","aemd","aemk","aenx","aeoh","aeqk","aeqm","aerd","aerf","aerj","aetn","aevs","aevv","aewp","aexv","afaq","afdf","affs","afig","afla","aflg","aflx","afqs","afrx","afux","afva","afvx","afwc","afzr","agfw","aggl","agjc","agkt","agkz","aglr","agsr","agss","agtk","agus","agvu","agxz","ahai","ahbh","ahen","ahjz","ahki","ahlk","ahmb","ahnn","ahyu","ahzn","ahzu","aiao","aibs","aibu","aicj","aidl","aigj","aiia","aikm","aima","aiml","aimr","aino","aiok","aiss","aiuo","aiut","aivn","aivy","aiwa","aixt","aizp","ajaa","ajam","ajbj","ajdn","ajdp","ajhp","ajka","ajxi","ajyp","ajzh","ajzq","akam","akdl","aked","akgr","akgt","akpo","akpz","aksr","akvz","akwp","akwq","akxc","alac","alak","alaw","alca","alcn","alfk","alie","aljw","alkg","allc","almr","aloq","alor","alpa","alqt","altq","alua","alub","alwf","alwj","alyf","amaw","amcj","amfk","amoy","ampu","amqj","amtj","amxj","andt","anet","aneu","anfm","angl","anhj","anjf","anmx","annr","anoq","anov","anqm","ansp","anyd","anzn","aobu","aofe","aogj","aogm","aohx","aoka","aonv","aopv","aoqq","aorg","aote","aoyn","apbv","apdy","aphv","apjq","apjx","apkk","apkp","apne","appd","appl","appr","aptc","apzr","aqbr","aqcc","aqcn","aqdm","aqfw","aqgp","aqgy","aqht","aqhx","aqkv","aqln","aqpf","aqpx","aqra","aqrd","aqsr","aqvu","argg","argh","ariq","arli","armq","arny","arrs","arsy","arum","aryo","arzg","arzq","asgc","asit","asiw","aske","asma","asmh","asnm","aspb","asrs","asth","asul","asvi","asvk","aszk","atbm","atds","atec","atgx","athr","atkt","atno","atph","atql","atws","auao","aufd","auhe","auja","aujp","auof","ausw","auux","auvq","avag","avas","avfs","avgv","avix","avnu","avqe","avug","avvr","avvv","avwl","avzb","awbu","awdi","awex","awfg","awhx","awir","awki","awku","awmn","awog","awpt","awrf","awrw","awsh","awvz","awze","awzx","axdh","axlp","axne","axpp","axsi","axtj","axuj","axvr","axxg","axzc","ayfj","ayfq","ayim","ayio","ayjx","aymf","aymu","ayqs","ayry","ayxm","ayzk","ayzx","azbx","azex","azfm","azlq","azlu","azqt","azsm","aztu","azvz","azxd","azxr","azyn","babg","bafj","bakq","bamh","bana","batr","bawg","bazd","bbag","bbct","bbgw","bbhn","bbkj","bbmt","bbog","bbow","bbqh","bbqi","bbqk","bbsg","bbtb","bbte","bbuz","bbwv","bbxn","bbxr","bbyp","bcab","bcae","bcay","bcde","bcfb","bcgj","bchj","bckq","bcll","bcmg","bcoj","bcoq","bcpa","bcuz","bcvw","bcwk","bcwq","bcxh","bdcd","bddt","bddv","bdgd","bdia","bdix","bdju","bdkp","bdlt","bdlu","bdqf","bdtb","bdtv","bdun","beax","bebk","bebu","bedb","bede","befw","behg","behh","beib","beke","belr","bert","bete","beus","beut","beve","beyb","beyj","bezh","bfar","bfbo","bfbx","bfdh","bfdk","bfdr","bfds","bfed","bfhc","bfjt","bfps","bfri","bfuy","bfxc","bfzu","bgak","bgbr","bgcf","bgdd","bgen","bgge","bghh","bghl","bghq","bgjb","bgrh","bgwe","bgwp","bgwr","bgwv","bgzv","bhhx","bhie","bhlz","bhrj","bhub","bhzr","bhzt","bhzy","biam","bica","bidp","biej","bifd","bigz","bilu","bimx","biqa","bitq","biuw","biwe","biwp","bjaa","bjen","bjey","bjgd","bjgk","bjhc","bjhi","bjhx","bjia","bjid","bjiq","bjkn","bjoa","bjph","bjpm","bjrd","bjri","bjrv","bjuu","bjyh","bjyu","bjzw","bkfy","bkoy","bkpr","bkqq","bksm","bkum","bkvh","bkvs","bkxi","blbz","blct","blej","blfo","blhg","bljl","blkj","blkq","bllk","blnk","blqm","blsj","blsu","bltm","bltt","blwu","blxi","blxx","blyk","blyp","blzd","bmbc","bmbe","bmdl","bmef","bmeh","bmgz","bmjr","bmly","bmmu","bmpv","bmqc","bmqm","bmrn","bmry","bmst","bmtu","bmzj","bnai","bnal","bnat","bncf","bnfd","bnfv","bnhw","bnhz","bnmo","bnnb","bnqi","bnqk","bnrg","bnrp","bnsl","bntl","bnto","bnxe","bnxm","bnzt","bobm","bokn","bolr","bonm","boov","boqb","bosd","botr","boub","bpcz","bpjn","bpkp","bpkz","bpll","bpmw","bpov","bpsl","bpwk","bpxb","bpyp","bqbs","bqbz","bqef","bqgi","bqia","bqid","bqil","bqke","bqlf","bqml","bqql","bqqp","bqqs","bqqv","bqru","bqst","bquz","bqvw","bqwm","bqws","bqxl","bqxn","bqxv","bqxw","brgy","briv","brkk","brld","brlj","brnw","bruv","brvg","brvu","bryb","bsat","bsdl","bshd","bsjy","bsll","bslw","bsmg","bsns","bsog","bsqj","bssd","bssq","bsst","bswe","btcs","btdp","btez","btfn","btgw","btid","btij","btir","btnw","btps","btpz","bttf","btuh","btxj","btzf","buao","bubm","bucf","bucj","budu","buen","buge","bugw","buhx","buiv","bulw","bumw","buoo","buop","buqy","busg","buxg","bvar","bvhx","bvii","bvkg","bvkh","bvki","bvpu","bvqa","bvxd","bvxj","bvxv","bvxx","bvzh","bwda","bwei","bwfg","bwgu","bwhz","bwko","bwkt","bwle","bwof","bwpi","bwqw","bwqx","bwto","bwtx","bwxp","bxad","bxdb","bxdj","bxep","bxfq","bxha","bxjp","bxjq","bxjr","bxkn","bxly","bxnd","bxnp","bxsz","bxxe","bxxn","bxzf","bxzm","bxzq","bybo","bycs","byjb","byju","byly","bynh","bypp","byrr","byrv","bysd","bytt","byuo","byxi","bzam","bzaq","bzbp","bzcw","bzgd","bziq","bzjo","bzkh","bzln","bzms","bznt","bzoq","bzpu","bztq","bztw","bzwl","cacc","cahy","caji","cajp","calo","cani","canp","caof","caot","capd","capx","cban","cbbs","cbbv","cbcm","cbgf","cbip","cbkj","cbmi","cbmr","cbmt","cbok","cbou","cbsq","cbwm","cbxr","cbyi","cbzj","ccav","ccbl","ccbz","cccg","cccj","ccet","ccfo","ccfq","ccgg","cchg","cchl","cchr","ccjp","cclg","ccoj","ccor","ccph","ccpq","ccrx","ccry","ccvq","ccwt","ccxb","ccxq","ccyk","cdfy","cdit","cdjq","cdjs","cdml","cdol","cdpj","cdpo","cdpw","cdra","cdrk","cdrz","cdsr","cdts","cdvf","cdvq","cdxm","cedc","cedm","cedt","cedz","cegt","cein","ceiv","cejx","ceoj","cepq","cerm","ceun","cevl","cevv","cewu","ceyj","cfbe","cfit","cfjc","cflz","cfol","cfrs","cfuh","cfxt","cfyp","cfzr","cgcg","cgch","cggt","cgjn","cgkh","cgoo","cgot","cgpv","cgqu","cgsq","cgsz","cgye","cgzl","chbz","chcy","chdd","chfo","chgm","chio","chji","chjt","chjw","chmh","chnb","chnm","chpz","chvy","chxy","chxz","chyo","cian","ciby","cidv","cihr","cijz","cimy","cinw","cjai","cjdd","cjdl","cjeu","cjfe","cjgb","cjgh","cjja","cjmt","cjsr","cjtj","cjuf","cjva","cjwo","ckbc","ckbu","ckck","ckdh","ckhm","ckjw","ckjy","ckkd","ckks","ckpk","ckpw","ckqk","ckrx","ckst","cktn","ckts","ckvg","ckxf","ckzh","clbx","clcs","clcu","clej","cleo","clgn","clhd","clnx","cloj","clox","clqf","clsu","cluu","clxe","cmbl","cmeb","cmgo","cmkn","cmlz","cmnq","cmpc","cmpo","cmta","cmuv","cmuw","cmxd","cmxe","cmyw","cngp","cnhn","cnhs","cnih","cnjj","cnnh","cnps","cnpv","cnqi","cnse","cnsm","cnul","cnxp","cnxt","codd","codg","coga","cojk","cojw","cosp","cotd","cotz","covb","covl","covm","cowg","cowj","coyx","cpan","cpay","cpbl","cpcp","cpfk","cpgl","cpgm","cpgw","cphw","cpii","cpjc","cpjl","cpls","cpmm","cpra","cpre","cprm","cpst","cqfl","cqod","cqos","cqrh","cqsm","cqve","cqvg","cqwp","cqwv","cqzp","crdr","crhp","crhz","crir","crit","crkh","crks","crqz","crsd","cruw","crxk","cryj","crzt","crzx","cscj","csdb","csfi","csfv","csgy","csis","cskc","cskv","csmi","csnd","csqn","csqw","cssg","cstd","cstl","csug","csvx","cszn","ctcv","ctgb","cthj","ctmw","ctpb","ctpd","ctpo","ctqg","ctto","cttz","ctuq","ctvj","ctvz","ctwj","ctxf","ctxh","ctxn","ctyw","ctzx","cubo","cuhb","cuhj","cuhp","cuhy","cujr","cuqe","cuqk","cutl","cuvq","cuwx","cuxy","cuzk","cuzq","cvci","cvcn","cvdj","cvdr","cvft","cvhr","cvhu","cvrr","cvtv","cvze","cwdj","cwee","cwfv","cwfw","cwma","cwmb","cwph","cwrz","cwwk","cwxw","cxbm","cxbt","cxdv","cxhv","cxkb","cxla","cxlm","cxmp","cxoa","cxpe","cxsx","cxvm","cxvp","cxzl","cyau","cybu","cycg","cydd","cydh","cydl","cydu","cygg","cyhe","cyhw","cykl","cyoi","cytc","cytr","cyxs","cyyg","cyyp","czay","czdv","czfa","czfo","czfp","cziv","czkg","czku","czle","czlx","cznd","cznx","czpw","czse","czwc","czws","czxd","czzk","czzu","czzz","dacv","danx","dapq","daqk","daqt","daso","dasp","dasv","dawl","dawv","daxc","daxd","dazj","dbbs","dbgi","dbox","dbrf","dbsd","dbvq","dbxk","dbzf","dcax","dcct","dcee","dcer","dcic","dcin","dciq","dckg","dclz","dcnd","dcok","dcoq","dcpp","dcrn","dcuh","dcul","dcvd","dcwn","dcxb","dczk","ddav","dddj","ddfc","ddjo","ddmr","ddof","ddqv","ddro","ddtb","dduu","ddyq","ddzd","debp","debs","decw","defg","degt","dehl","dejb","dely","demx","deoe","deov","deph","derc","deup","deur","deuw","dewf","dfba","dfcg","dfdn","dfgt","dfiz","dfjx","dfjy","dfkj","dfko","dfli","dfsy","dftj","dftk","dftl","dgaq","dgcq","dgfl","dgii","dgjl","dgjq","dgkg","dgmc","dgnm","dgpt","dgqt","dgsr","dgzq","dhas","dhbq","dhff","dhfh","dhfx","dhhd","dhkk","dhlq","dhlv","dhrt","dhss","dhus","dhyu","dhyy","dico","dihg","diia","dijk","diki","dikm","dimm","dior","dirg","diua","diue","diuu","dixd","dixg","djal","djbz","djfc","djfr","djfy","djhe","djie","djll","djlp","djmb","djmf","djnm","djop","djqd","djse","djsm","djuv","djvz","djwi","djyn","dkae","dkbq","dkel","dkic","dkjd","dkjv","dklq","dkmc","dknz","dksn","dksp","dkun","dkvr","dkwf","dlci","dlkm","dlky","dlmt","dlof","dlqa","dlrl","dlwt","dlyt","dlzc","dmby","dmcu","dmjw","dmmx","dmrh","dmtj","dmtk","dmui","dmum","dmuy","dmwq","dmxi","dmxp","dmxs","dnad","dnbs","dnhn","dnhw","dnic","dnjt","dnlu","dnoa","dnod","dnon","dnor","dnpb","dnph","dnqu","dnsp","dnsr","dnua","dnuz","doam","dobn","doeb","dohc","domo","donn","doqv","doue","dovc","dowx","doxz","dozc","dpbj","dpez","dpfp","dpgj","dpif","dpkv","dpoi","dprs","dpsm","dptw","dpvi","dpwd","dpwh","dpwm","dpxq","dqay","dqbk","dqbx","dqdh","dqeg","dqgl","dqhz","dqie","dqka","dqlo","dqnp","dqns","dqom","dqqc","dqrf","dqrk","dqsn","dqtd","dqtu","dquh","dqul","dqum","dqwl","dqxb","dqxp","dqzq","dqzv","drbh","drgj","drgm","drgp","drgv","drjz","drnl","drpv","drrn","drtf","dryy","drzc","drzf","drzx","dsad","dsaj","dsak","dscn","dsdy","dsey","dsfy","dsif","dsjm","dsla","dsld","dsna","dspm","dsty","dswe","dsxu","dsyt","dtej","dtfi","dtfj","dtgo","dtio","dtiu","dtji","dtjz","dtlg","dtmh","dtmk","dtml","dtwb","dtwm","dtza","duab","dudr","dueu","dugy","dulx","dunz","duom","duph","dupl","duto","duws","duxd","duyo","duys","dvad","dvck","dvev","dvip","dvlh","dvli","dvmq","dvmt","dvns","dvrv","dvvb","dvwg","dvxn","dvxo","dvyn","dwbb","dweu","dwfe","dwfh","dwfy","dwgz","dwho","dwhp","dwlf","dwol","dwqc","dwqj","dwrs","dwsf","dwsx","dwtx","dwus","dwuw","dwzr","dwzw","dxcc","dxdc","dxdd","dxdn","dxef","dxfe","dxiq","dxla","dxnp","dxoe","dxpj","dxpx","dxta","dxxk","dxzw","dyaq","dyba","dycw","dyeh","dyfb","dyix","dykq","dykv","dymt","dyoi","dyqh","dyqs","dysk","dyyn","dyzu","dzbu","dzfo","dzjy","dzlr","dzms","dznd","dzoq","dzps","dzqg","dzqy","dzrc","dzvi","dzvl","dzwd","eada","eadf","eaeq","eaez","eaff","eagf","eahw","eaio","eajs","ealq","eanh","eank","eaps","eaxv","eazg","eazz","ebaz","ebdi","ebgj","ebkd","ebkl","eblr","ebmj","ebnw","ebob","eboz","ebsh","ebzn","ecbq","echi","ecja","eckz","ecmz","ecsx","ecwj","edal","ediz","edme","edmf","edoh","edtg","edtx","edwy","edxp","eeaw","eeew","eejr","eelb","eelt","eelz","eene","eepv","eepz","eeqy","eesj","eesn","eeso","eetw","eewc","eeyn","eeys","eezm","effl","efhl","efhn","efhp","efhx","efjq","efkn","eflx","efmq","efnl","efnp","efqn","efsn","efun","egas","egeu","eghv","egik","egiv","egkp","eglw","egpb","egrb","egro","egxn","egye","ehdg","ehgg","ehhh","ehhq","ehih","ehiu","ehjq","ehnv","ehoe","ehuw","ehwk","ehyq","eiba","eicd","eict","eief","eifq","eige","eigf","eigh","eiix","eije","eikc","eils","eilz","eiok","eisv","eivn","eivv","eiwk","ejab","ejad","ejag","ejbd","ejbk","ejcn","ejdq","ejds","ejeb","ejex","ejfl","ejgz","ejjx","ejkj","ejkq","ejlc","ejne","ejos","ejtl","ejul","ejvv","ejyl","ekes","ekkh","ekku","ekla","ekml","ekne","ekrq","ekru","eksl","ekvp","ekxe","ekyl","elac","elch","elcn","elhp","elmj","elmp","elrx","elsj","eltr","elub","elxf","elyj","embq","emds","emgs","emvs","emwy","emzi","emzr","enao","endc","endi","enfy","enhz","enix","enkt","enlm","enmc","enmq","enuh","enuo","enup","enyw","eoat","eock","eocp","eocu","eoeg","eogt","eojc","eolw","eomg","eoqw","eowb","eoxy","eoyk","eozn","epao","epcs","epda","epeo","epey","epfw","epke","eplx","eppa","eppy","epre","epth","epuw","epzw","eqan","eqbg","eqcj","eqgd","eqjb","eqln","eqmt","eqmu","eqmz","eqpu","eqpz","eqra","eqxd","eqxt","eqye","eqyw","eqza","eqzx","erfo","erkt","erlj","erqf","erue","erxj","esbn","eseu","esfg","esfh","eshb","esht","esip","eslr","eslz","esnn","esnx","espz","esty","esvh","eswb","esxe","esxo","esya","etcj","etfr","etgd","etgz","ethd","etiq","etpe","etqi","etwr","eudo","euhq","euia","euiq","eumq","euny","euon","euos","eurb","eurh","eutk","euui","euul","euus","euve","euvk","euwc","euyb","euyk","euzb","euzp","euzw","evcb","evfp","evgl","evgy","evif","evlr","evnf","evqe","evrn","evza","ewae","ewat","ewbe","ewiz","ewju","ewki","ewlv","ewmi","ewnh","ewni","ewot","ewpc","ewrg","ewrz","ewxq","ewyo","ewzk","exat","exbk","excb","exeo","exfd","exfw","exgj","exin","exjg","exjt","exmd","exmh","exni","expd","exsc","exuu","exyg","eyba","eycb","eydm","eydr","eyft","eyhw","eyih","eyjf","eyll","eynu","eyoa","eypv","eytm","eyue","eyvj","eyvp","eywh","eyxo","ezee","ezkf","ezli","ezma","ezqy","ezxx","ezyj","ezyv","fadm","fahl","falb","fals","fasy","fato","fatz","faue","fazl","fbey","fbfz","fbga","fbgf","fbjj","fbju","fbol","fbon","fbps","fbqx","fbtk","fbtt","fbue","fbxz","fbzv","fcai","fcfj","fchx","fcin","fcja","fckz","fclb","fclv","fcmq","fcmt","fcos","fcss","fcta","fcux","fcvc","fcyk","fczk","fddj","fdgl","fdgv","fdhi","fdkm","fdkw","fdnd","fdnq","fdpg","fdqi","fdsy","fdwj","fdwr","fdxb","fdxx","fdzl","fecn","fedo","feek","fefw","fegd","fehi","fekg","fekp","felp","feno","feon","fers","fetb","fevj","ffbu","ffcr","ffek","ffhi","ffrb","ffsi","ffsx","ffta","ffyc","ffym","ffyx","ffzt","fgcm","fgdj","fgdt","fgjq","fglj","fglw","fgnh","fgqk","fgqn","fgsw","fgtm","fgvj","fgyo","fhak","fhax","fhca","fhcc","fhdw","fhej","fhgf","fhjf","fhkj","fhkq","fhkr","fhli","fhlz","fhmv","fhor","fhtn","fhug","fhul","fhwb","ficx","fiej","fies","figk","fiid","finf","fipr","fiqh","firt","fitt","fiuo","fivq","fiwe","fixj","fiyq","fizn","fjcb","fjdr","fjfj","fjfl","fjfo","fjgt","fjmw","fjne","fjqi","fjrr","fjrz","fjwa","fjxk","fjxz","fjyz","fkbr","fkez","fkgz","fkib","fkjs","fkkn","fkns","fkor","fkph","fkpk","fkpn","fkqx","fkrc","fkrk","fktw","fkup","fkvh","fkye","fkzl","flfw","flke","flkh","flkm","flll","flma","flmt","flmy","flrp","fltn","flvy","flwh","flyg","flzl","fmaq","fmbi","fmej","fmgs","fmin","fmjv","fmly","fmmb","fmmm","fmmn","fmnt","fmph","fmrb","fmtu","fmvd","fmxj","fnep","fngp","fnhb","fnkr","fnkz","fnla","fnme","fnmu","fnpa","fnse","fnsn","fntv","fnyn","fnyr","foax","fobe","fofx","fogq","fohr","fohy","fokr","foks","fokt","foku","folq","fopy","foqx","forc","fost","fotv","fovc","fpac","fpae","fpat","fpbs","fpdp","fpfx","fpgn","fpiu","fpjb","fpka","fpmm","fpov","fppr","fpqy","fpre","fprh","fprz","fpsh","fpsl","fqaa","fqbv","fqda","fqdo","fqfg","fqie","fqis","fqiv","fqjc","fqje","fqkx","fqme","fqnq","fqoi","fqux","fqwg","fqwy","fqxo","frbh","frbi","frcy","frey","frff","frgy","frhd","frhf","frkf","frkm","frkx","frkz","frmd","frnj","frnq","frns","frob","froh","frop","froq","frqv","frrh","frsd","frsp","frvv","frwb","frww","frxa","fryt","fryu","fsby","fscj","fsef","fshm","fsii","fsjb","fskh","fskj","fskw","fsmu","fspg","fssu","fswa","fsxi","ftbp","ftff","ftfj","ftgm","fthe","fthn","ftii","ftoa","ftrk","ftsp","ftvf","ftwg","fuas","fubo","fucx","fucy","fugl","fugt","fugy","fuhj","fukg","fukq","fulf","fuok","fusy","fuud","fuum","fuwz","fuyv","fvbe","fvcb","fvdq","fvfr","fvfu","fvgt","fvid","fvim","fvkp","fvmq","fvox","fvoz","fvpp","fvra","fvrb","fvrc","fvso","fvzo","fwad","fwap","fwca","fwdc","fwds","fwew","fwud","fwum","fwuy","fwvu","fwxj","fxdm","fxen","fxfh","fxfi","fxgt","fxsc","fxtw","fybe","fycl","fycx","fydr","fyed","fyeo","fyhx","fylt","fyoq","fyqb","fyss","fyuf","fyvz","fyyh","fzbt","fzdi","fzig","fzlo","fzmi","fznc","fzni","fznm","fzpp","fzre","fzvi","fzvm","fzyi","gaac","gaaz","gadn","gaij","gama","gamt","gaos","gaow","garj","gasq","gavk","gavq","gawg","gbac","gbdj","gbez","gbih","gbkq","gbld","gblk","gbnj","gboe","gboh","gbvg","gbwo","gbwx","gbye","gbzf","gcar","gchv","gcke","gcln","gclr","gcmk","gcpb","gcrw","gcvd","gcvt","gcwl","gczh","gdbs","gdco","gdfq","gdlc","gdnv","gdrh","gdsq","gdta","gdty","gdyn","gdyw","gebf","gecx","gedg","geew","gefz","gelr","gelz","genb","geyh","gfbi","gfde","gfdv","gfge","gfgx","gfhv","gfjh","gfjz","gflk","gfsg","gftp","gfuq","gfvt","gfwv","ggfb","gghc","gghk","gghn","gglm","ggnb","ggpp","ggqx","ggrg","ggtw","ggun","ggvi","ggwb","ggzo","ghas","ghbf","ghcc","ghda","ghdd","ghdh","ghko","ghkq","ghku","ghlu","ghmi","ghpi","ghvz","ghxu","ghys","ghzz","giai","giaj","gibx","gicl","gict","gidc","gifk","gigd","gijs","giln","gimi","ginm","gipq","giub","giux","gjfe","gjjn","gjlm","gjoa","gjol","gjoz","gjpy","gjrk","gjrw","gjtv","gjtw","gjul","gjur","gjvw","gjwk","gjxy","gjyh","gjzd","gjzp","gkcl","gkcn","gkeh","gkff","gkfh","gkgx","gkhg","gkks","gkmf","gkmg","gkpb","gkrr","gkvu","glah","glbk","glch","gldb","glep","glfe","gljh","glmj","glni","glnz","glot","gloz","glph","glwt","glyh","glyk","glyu","glzz","gmhc","gmkd","gmlh","gmmg","gmmq","gmpb","gmpc","gmpi","gmpo","gmpt","gmqg","gmtv","gmwu","gmxl","gmxz","gmyk","gmzs","gnds","gnfz","gnlr","gnmc","gnnh","gnni","gnqn","gnul","gnuy","gnvz","gnwj","gnzm","goae","goam","gobg","gobr","godk","gohg","goki","gokp","goot","gooz","gopd","gopg","gord","goth","goys","gozx","gpbb","gpcs","gpdm","gpdw","gpgt","gpkk","gpkt","gplc","gpll","gpoj","gpsc","gpuu","gpwb","gpzi","gqar","gqgw","gqiq","gqit","gqle","gqlf","gqli","gqlm","gqoj","gqpd","gqum","gqvv","gqyx","gqzb","gqzf","grah","grec","grfi","grfm","grhj","grhx","grnc","grnm","grnn","grnq","grov","grtx","gryu","grzd","grzv","gsca","gscd","gsct","gscy","gsew","gsfz","gslr","gsnf","gsqt","gsrz","gssl","gsuv","gszf","gszi","gszy","gtcz","gtdp","gtev","gtfl","gthn","gtkr","gtmt","gtnh","gtom","gtqo","gtqq","gtqv","gtrt","gtsf","gtvr","gtwf","gtxj","gtyu","gueb","gueo","gufb","gugz","guik","gumi","gumr","gupj","gurg","gusx","guti","guyh","guyu","gvao","gvcf","gvci","gvdz","gvej","gvfl","gvgu","gvij","gvjd","gvjg","gvkx","gvln","gvok","gvox","gvue","gvus","gvzp","gwhj","gwlu","gwnz","gwom","gwpw","gwss","gwtd","gwwj","gwxo","gwym","gxam","gxaz","gxbk","gxbz","gxdf","gxdu","gxgq","gxgv","gxhu","gxhw","gxii","gxio","gxjg","gxly","gxmi","gxnt","gxow","gxpn","gxpz","gxrv","gxsg","gxwk","gxww","gxxc","gxxq","gxxs","gxxw","gydn","gydv","gyfj","gyjr","gykl","gyqb","gyrf","gyrw","gytm","gywl","gyzc","gzas","gzbl","gzdj","gzea","gzir","gzkj","gzml","gznq","gzpx","gzqi","gzso","gzsz","gztm","gzxv","haah","haeo","hagw","hahm","haia","haif","hakl","hari","hase","hatk","haus","havh","hazg","hbaj","hbey","hbgb","hbgm","hbiv","hbqq","hbrq","hbtp","hbtw","hbuq","hbuu","hbxf","hcdj","hcdr","hcdx","hcef","hcgo","hchp","hcji","hckm","hcku","hcma","hcnp","hcnz","hcoa","hcom","hcos","hcxh","hcxi","hczk","hdbp","hdca","hdel","hdex","hdft","hdnp","hdrz","hdty","hdum","hdwf","hdww","heay","hecl","hedg","hehw","heih","heni","hepr","herr","hesx","hesy","heuj","hevh","hevw","hews","heyi","hfcu","hfcz","hfdi","hfdp","hfef","hffp","hfix","hfni","hfof","hfpf","hfqe","hfqi","hfrg","hftg","hfth","hfvm","hfxb","hfxf","hfyw","hfzb","hgaw","hgbj","hgea","hged","hgen","hgft","hggm","hghk","hghn","hgip","hgkg","hgkn","hgna","hgnp","hgpc","hgru","hgtu","hguu","hguz","hgwn","hgzj","hgzu","hhbm","hhbp","hhcj","hhdb","hhmb","hhmh","hhmu","hhny","hhpo","hhpp","hhqj","hhqy","hhwf","hhxe","hhyr","hhyw","hiav","hiev","higq","hihk","hikn","himg","himw","hion","hipx","hipy","hisa","hitc","hitp","hiug","hiuw","hivm","hiwr","hiyw","hjdd","hjef","hjer","hjhp","hjiq","hjke","hjkz","hjlv","hjnk","hjns","hjoi","hjql","hjqz","hjrw","hjsr","hjtk","hjwd","hjzn","hjzt","hkcq","hkdq","hkfy","hkgy","hkhh","hkkz","hklm","hknp","hknz","hkoi","hkpw","hkqw","hkra","hkrk","hkty","hkwa","hkwi","hkyp","hlbl","hlce","hlct","hlfu","hlhl","hlho","hllg","hlnb","hlpk","hlqs","hltd","hlxq","hlzn","hlzz","hmap","hmbb","hmgu","hmkh","hmow","hmti","hmto","hmuf","hmwn","hmxl","hmzj","hncv","hnez","hnie","hnjk","hnkz","hnoo","hnpn","hoad","hobh","hocm","hodv","hofb","hojs","hojy","hokz","holg","homu","honb","honp","hoom","hoph","hopj","hosd","houi","hous","hovf","hovi","hoxt","hoyr","hpbg","hpbn","hpcg","hpcl","hpgh","hphe","hpjt","hplh","hpmq","hpoj","hppz","hprv","hpse","hptu","hpwo","hpxs","hqdw","hqfd","hqfk","hqfl","hqfy","hqhy","hqla","hqlm","hqmu","hqnd","hqou","hqpz","hqqf","hqsh","hquc","hquu","hqvo","hqww","hqxk","hqzs","hrav","hrdk","hrgm","hrit","hrjs","hrkd","hrkk","hrkv","hrmb","hrqb","hrwa","hrzp","hrzw","hsfv","hsgv","hsjl","hskl","hsku","hskw","hsnc","hsnk","hspy","hsqm","hsru","hstd","hsvu","hsxx","hsze","htae","htas","htbc","htbj","hteb","hten","htey","htfr","htfs","htgy","hthw","htjf","htnt","htoj","htrc","httf","htug","htuh","htuo","htvx","htxw","huaj","hufr","hugn","hujk","husz","huui","huvm","huyp","huzb","huzt","hvae","hvco","hvec","hvek","hveo","hvho","hvjx","hvlb","hvlq","hvoa","hvoo","hvoq","hvov","hvtv","hvuf","hvus","hvxd","hvzu","hwao","hwdf","hwgd","hwhc","hwie","hwkv","hwmg","hwmv","hwoh","hwqh","hwqu","hwrj","hwsp","hwsu","hwyt","hxbc","hxcw","hxfl","hxnc","hxpm","hxpx","hxsh","hxvc","hxvy","hxwo","hxyg","hxyn","hxza","hxzs","hybz","hych","hygp","hygq","hygz","hyhn","hypm","hyrt","hysj","hyvq","hyzf","hzch","hzfu","hzjf","hzkk","hzmn","hzob","hzqz","hzrg","hzui","hzxx","hzze","hzzx","hzzz","iaap","iadf","iaff","iajg","iakw","iaom","iasl","iasx","iats","iatx","iava","iavk","iavw","iaxs","iayx","ibfl","ibiv","ibiy","ibjk","iblu","ibnd","ibnf","iboe","ibph","ibqb","ibqq","ibri","ibxk","ibxp","icar","icbp","iccw","icgb","icgs","iclp","icme","icsh","ictz","icuf","icuq","icut","iczu","idcp","idds","iddz","ided","ideh","idhg","idhq","idie","idkd","idnt","idnw","idqz","idtn","iduo","iduu","idvy","idyt","iedn","ieei","iefu","iemf","iemr","ieod","iepi","iepx","iequ","iesn","iesp","iesq","ieut","ieuz","ievd","ievj","ifbg","ifcf","ifcq","ifdq","ifej","ifft","ifht","ifiy","ifjc","ifjr","ifkr","ifni","ifob","iftt","ifui","ifxj","ifyv","ifzp","ifzq","ifzu","igaj","igbf","iged","igft","iggj","iggl","iggs","igil","igiz","igmm","igoa","igrs","iguh","igyu","igzu","ihbj","ihcf","ihfv","ihgm","ihie","ihkf","ihkg","ihnr","ihod","ihrf","ihsb","ihsg","ihsv","ihsw","ihuw","ihvm","ihwf","ihzb","iiaw","iici","iicn","iidp","iigc","iign","iijd","iikf","iikt","iiky","iile","iilq","iinf","iiom","iior","iioy","iipg","iiqt","iire","iitu","iizo","ijak","ijeb","ijeg","ijer","ijet","ijge","ijju","ijlh","ijlm","ijns","ijqj","ijrv","ijuk","ijxc","ijzr","ijzy","ikac","ikba","ikdi","ikdu","ikeb","ikgo","ikgs","ikit","ikjy","ikko","iklr","iklv","ikmo","ikuj","ikxy","ikym","ilal","ilay","ilbh","ilcj","ilkr","ilng","ilnn","ilqo","ilrk","ilrv","iltk","ilvg","ilwd","ilwf","ilyt","ilzi","imau","imav","imcm","imdb","imeh","imey","imfh","imgx","imjn","imjz","imla","imlc","imoe","imqd","imrq","imrt","imtn","imwg","imwp","imxq","imyq","imzj","indw","infm","infv","ingd","ingn","ings","inhr","inhv","inhx","iniw","inje","injv","inmh","innj","inpb","inpk","inqo","inzf","ioao","iodl","iody","iokt","iolv","iomw","ionc","iopj","iopo","iosd","ioty","ioyc","ioyz","ipew","ipfu","ipif","ipmk","ipmq","ipnv","ippc","ippd","ipqi","iprn","ipxy","ipyz","iqcy","iqex","iqfc","iqfk","iqfx","iqlr","iqns","iqri","iqrw","iqso","iqvb","iqwk","iqxp","iqxx","iqyl","iqyy","iqzy","iral","irdk","irjk","irlw","irmp","irna","iroz","irpo","irux","iryg","iryy","isaf","isao","isbz","isds","isfc","ishi","isif","isml","isoo","isqe","isqr","isrt","istg","istt","isvh","isxy","isza","iszx","itct","itfn","ithi","itip","itiw","itmp","itpf","itpl","itqa","itud","itwg","ityx","iubd","iubi","iudw","iugh","iuhx","iuie","iulz","iuoc","iupm","iuth","iuto","iuvt","iuwx","ivaf","ivbo","ivcd","ivcf","ivea","ivfy","ivhg","ivjj","ivmo","ivmp","ivok","ivrk","ivxh","ivxx","ivyb","ivys","iwek","iwfo","iwim","iwnh","iwnj","iwpo","iwth","iwum","iwvr","iwvv","iwwh","iwxe","iwxs","ixac","ixdf","ixgz","ixiy","ixlo","ixly","ixml","ixor","ixvc","ixvr","ixwz","ixyo","ixyx","iyaz","iyci","iydq","iyfj","iyfl","iylh","iyma","iymo","iynr","iypx","iyrb","iysh","iyyx","iyyy","iyza","izak","izat","izcy","izep","izfk","izjn","izoe","izoz","izqy","izrm","iztw","izut","izvk","izwg","izyi","jacx","jada","jadu","jafi","jafo","jaig","jaka","jamf","jaso","java","jazc","jazp","jbbd","jbbv","jbeb","jbfv","jbfx","jbgn","jbiu","jbiw","jbmk","jbnf","jboe","jbpw","jbqf","jbqg","jbql","jbru","jbtl","jbwf","jbwl","jbwx","jcah","jccg","jcdd","jcdu","jcew","jcgw","jchh","jckb","jclq","jcmo","jcnp","jcoh","jcpr","jctj","jcux","jcvl","jcwe","jczh","jdaf","jdcd","jdce","jdhf","jdhh","jdhi","jdhk","jdlz","jdmn","jdmp","jdog","jdpb","jdtb","jduj","jdwd","jdxv","jdyj","jeah","jeak","jecf","jefs","jegc","jegg","jehp","jejc","jeje","jend","jewx","jfal","jfdv","jfff","jfgb","jfie","jfjv","jfkr","jfkw","jfox","jfqf","jftz","jfvq","jfwc","jfwg","jgbl","jgef","jgfd","jgfg","jggy","jght","jgir","jgkb","jgmw","jgom","jgqg","jgte","jgug","jgus","jgvg","jgvj","jgvu","jgwk","jgxl","jgzb","jhad","jhbj","jhey","jhfb","jhfr","jhis","jhli","jhnn","jhrw","jhse","jhsl","jhss","jhug","jhuy","jhwy","jhxc","jhxf","jhzb","jibt","jifk","jihj","jiib","jiin","jilh","jilm","jimg","jins","jiob","jioi","jipl","jirr","jiur","jiyj","jjah","jjez","jjgf","jjik","jjkp","jjmi","jjnq","jjph","jjrn","jjtb","jjum","jjxm","jjyf","jjyj","jkaf","jkaq","jkcw","jkem","jkgl","jkia","jkjs","jkkw","jklf","jklm","jklo","jkre","jksa","jkth","jktr","jkun","jkwp","jkww","jkya","jkyy","jlbs","jlea","jlep","jleq","jlhj","jlkr","jlmb","jlni","jlse","jltj","jlwi","jlyn","jlza","jlzt","jman","jmbb","jmbi","jmbo","jmcm","jmem","jmev","jmgy","jmiy","jmke","jmkx","jmnt","jmpv","jmqe","jmuj","jmvo","jmxn","jmxz","jmyv","jnbb","jnby","jncr","jndb","jnfz","jniw","jnjf","jnke","jnlg","jnln","jnok","jnyh","jnzp","joaq","jodo","joen","jofi","jofw","john","jojp","jokl","joks","jonj","jooa","joqj","josl","jotv","jovr","jovs","joyy","jozj","jozr","jpah","jpbn","jpdn","jpln","jpnk","jpnn","jpou","jpqh","jpsp","jptg","jptj","jpwf","jpwn","jpxn","jqal","jqbi","jqcl","jqer","jqfi","jqjw","jqlk","jqnx","jqob","jqqh","jqro","jqtb","jqur","jqvk","jqvp","jqxn","jqyx","jqzz","jraf","jrap","jrbs","jrdn","jrgj","jriq","jrky","jrlu","jrmy","jrnu","jrnx","jrps","jrtn","jrvt","jrzl","jscm","jsdl","jsey","jsfb","jsfs","jsku","jsnj","jsoa","jsoq","jsry","jssi","jsvz","jsxj","jsyn","jszl","jteg","jtgh","jthm","jthq","jthv","jtjb","jtkd","jtlb","jtmw","jtpo","jtql","jtuw","jtwm","jtwq","jtyd","jtym","juaa","judh","juen","jueq","jufw","juij","juis","julo","juqr","jutx","jutz","juua","juxy","juzl","jvbl","jvbm","jvbo","jvcb","jvix","jvka","jvkv","jvlg","jvqc","jvst","jvtd","jvtq","jvwz","jvyf","jvzi","jvzp","jwau","jwcc","jwhh","jwhr","jwjq","jwli","jwoy","jwwg","jwwz","jwxf","jwxg","jwzx","jxbd","jxdq","jxdu","jxfo","jxfr","jxfz","jxiu","jxje","jxnf","jxrs","jxsu","jxsy","jxtm","jxyj","jxyq","jxzo","jybk","jycw","jydg","jyhe","jyhv","jyjd","jyku","jykv","jylo","jyrh","jyvc","jyxb","jyzv","jzav","jzbt","jzbu","jzej","jzfy","jzgj","jzgs","jzib","jzij","jzkq","jznz","jzoh","jzqh","jzsr","jzvd","jzwy","jzxh","jzzc","jzzy","kaba","kaby","kaci","kafd","kafh","kajf","kakc","kamw","kaun","kavy","kaxv","kazv","kbau","kbci","kbfw","kbiv","kbnt","kbop","kbph","kbtn","kbts","kbvg","kbvu","kbxw","kbyv","kcct","kcda","kcfw","kcik","kciu","kcjx","kckk","kcmx","kcnd","kcoa","kcpb","kcpp","kcqm","kcrr","kcru","kcso","kcwv","kcya","kdad","kdav","kddh","kdef","kdgn","kdhc","kdju","kdjy","kdke","kdkn","kdlk","kdne","kdog","kdtw","kdyc","kdyl","keaq","kebx","kebz","kecg","kede","kego","kegq","kekj","kenl","keol","keox","kesf","kevf","kexj","kfbh","kfbi","kfda","kfdq","kfdz","kfgt","kfjq","kfko","kfns","kfqg","kfqs","kfss","kftp","kfuw","kfvg","kfxs","kfyt","kfze","kgbv","kgcd","kgdf","kgdj","kgdo","kggn","kgip","kgix","kgji","kgkv","kgle","kglz","kgmu","kgnu","kgok","kgoq","kgph","kgzf","kgzw","khav","khdr","khej","khep","khez","khfd","khlx","khmq","khmt","khnc","khod","khpm","khrx","khwa","khxc","kibr","kiht","kiig","kijk","kijr","kikp","kilj","kiok","kioq","kipv","kird","kisb","kisq","kitu","kivo","kiwu","kizc","kizs","kjbz","kjch","kjdf","kjdl","kjhx","kjid","kjii","kjir","kjll","kjlm","kjop","kjpm","kjpt","kjrx","kjtg","kjut","kjwy","kjxh","kjyg","kjyi","kjyv","kkav","kkdy","kken","kkhl","kkhn","kkja","kkot","kkxg","kkyd","klbv","klct","klda","klfw","klgg","klix","kljv","klny","klpf","kltj","klvc","klve","klvr","klxg","kmba","kmbl","kmbw","kmdh","kmfo","kmhc","kmjj","kmol","kmor","kmpb","kmru","kmrv","kmux","kmwv","kmzr","knak","kndd","kndf","kndg","knem","knim","knjd","knpj","knuz","knwf","knxb","knxy","kobe","kodo","kogr","kogt","koiw","kolx","kome","komp","konz","kosx","kotg","kott","kout","kowk","kpap","kpbj","kpct","kpeo","kpie","kpig","kpjc","kpjh","kpli","kplx","kpmy","kprl","kpti","kptq","kpuf","kpwm","kpwz","kqan","kqhp","kqjh","kqjz","kqla","kqlt","kqmn","kqnn","kqoj","kqtg","kqva","kqzh","krbd","krcc","krcf","kreq","krff","krga","krhq","krhw","krhx","kriu","krjn","krkt","krlj","krow","krqb","krsc","krts","kruj","krwv","krxb","krxe","ksdm","ksez","ksgs","ksgx","kspj","ksqb","ksse","kssv","ksvb","kswl","ksxe","ksym","kszg","ktbm","ktcc","ktcv","ktin","ktiv","ktiy","ktjo","ktkh","ktll","ktox","kttp","ktun","ktwu","kudz","kuia","kukg","kukk","kulo","kutn","kuuk","kuvx","kuwo","kuwq","kuxi","kuzo","kvbv","kvdg","kvdx","kvfs","kvhh","kvhn","kvla","kvmq","kvog","kvuc","kvvc","kvyt","kvzq","kwbg","kwde","kwdo","kwdp","kwgf","kwhh","kwig","kwjd","kwnf","kwqn","kwws","kwxi","kxba","kxef","kxek","kxfz","kxga","kxgp","kxgx","kxmm","kxnb","kxnp","kxpe","kxqb","kxuc","kxyp","kyad","kycq","kyhe","kyhx","kyic","kykq","kykt","kyll","kyns","kyps","kyso","kywe","kywz","kyyb","kzat","kzav","kzcz","kzdm","kzhq","kziq","kzky","kzln","kzmf","kzrr","kzrv","kztd","kztn","kzul","kzvq","labp","lacf","lacp","lacw","ladc","lafr","lahw","lamw","laqr","larx","lasi","lasn","lasu","laxx","layz","lbgc","lbiv","lbjp","lbko","lbos","lbqq","lbvg","lbxt","lbyn","lbze","lcbi","lcfs","lchn","lcky","lclf","lcmt","lcqw","lcsy","lcuv","lcvj","lcwg","lcws","lcyp","lcza","ldar","ldbf","ldbt","ldca","lddl","lddq","ldee","ldir","ldjp","ldjx","ldlw","ldma","ldnm","ldpj","ldpl","ldrl","ldrq","ldtw","ldus","ldzz","leba","lebj","ledo","ledx","lefi","legu","lehg","lehp","leld","lemw","lerh","lesq","lesv","levn","lexb","lfau","lfbb","lfbu","lfer","lfhb","lfhn","lfjn","lfkj","lflu","lfme","lfqg","lfql","lfxi","lfxs","lfxz","lfza","lgda","lggb","lgit","lgle","lgou","lgsy","lgvh","lgwe","lgzj","lhdg","lhdu","lher","lhfh","lhfi","lhiz","lhna","lhni","lhnl","lhnm","lhoc","lhqq","lhro","libc","licf","lict","liek","ligv","lihv","liif","lije","lijs","likm","liky","lilg","limv","linc","linv","lior","lios","lirh","lisn","litr","ljbh","ljcb","ljfv","ljgg","ljjk","ljlf","ljlh","ljnp","ljqe","ljti","ljtp","ljtr","ljuf","ljxg","ljxs","ljzm","lkdc","lkei","lkle","lklf","lkmz","lkoc","lkrj","lkrk","lksd","lkub","lkwm","llcw","llcx","lldn","llju","llla","llmb","llmi","llse","llsr","llss","llsx","lltf","lluh","llwo","llym","llzb","lmab","lmaj","lmaw","lmdl","lmfv","lmiy","lmle","lmsu","lmvm","lmxn","lmzy","lnaj","lnbs","lnfu","lnfv","lngo","lngu","lnir","lnjk","lnmz","lnoq","lnua","lnux","lodl","loey","loez","lofs","lofy","logd","lohy","loiq","lojd","lojw","lokq","lonj","lopb","lopz","loqx","loti","lowc","lowm","loxq","loyf","loys","lpaj","lpca","lpht","lpiq","lpir","lpjb","lpjk","lpks","lplh","lpmp","lppa","lppz","lprg","lpsz","lpta","lpti","lpwu","lpxf","lpyb","lpyc","lpyq","lqax","lqcx","lqed","lqei","lqfx","lqjj","lqlx","lqmm","lqpt","lqqs","lrbr","lres","lrgf","lrgp","lrjz","lrki","lrlv","lrma","lrmd","lrmh","lrmj","lrmy","lrnv","lrnx","lrpa","lrpx","lrqi","lrrg","lrsc","lrtv","lrvj","lrvm","lrzl","lsar","lscg","lsif","lsjg","lskv","lsna","lssm","lsvj","lswc","lsxz","lszw","ltaz","ltdk","ltdt","ltgp","ltgs","ltjs","ltme","ltom","lton","ltou","ltri","ltto","ltvd","ltyn","ltyq","lubd","lubl","lufd","lufz","luji","lukb","luln","luns","luom","luqx","luub","luvi","luxu","luyi","lvbr","lvgs","lvhv","lvoy","lvpp","lvpt","lvrc","lvrq","lvse","lvvb","lvxg","lvxq","lwad","lwfe","lwok","lwqs","lwss","lwtk","lwyl","lwzt","lxam","lxap","lxeq","lxfl","lxjw","lxjz","lxkp","lxla","lxlv","lxni","lxrb","lxui","lxuk","lyeh","lygn","lygw","lyiz","lyjo","lykc","lykq","lymz","lyod","lyrv","lyvq","lyvr","lyxv","lyyb","lyzn","lzag","lzci","lzeo","lzgs","lzjg","lzmw","lzoe","lzoi","lzom","lzqx","lzsg","lzuf","lzvv","lzwn","maab","madw","mafz","makt","maod","maof","maqv","maui","maxr","mbbe","mbbo","mbbr","mbdm","mbgf","mblm","mbls","mbqh","mbqm","mbqw","mbsl","mbso","mbtg","mbtj","mbtl","mbwg","mbwo","mbym","mbze","mcan","mcds","mcfq","mcgw","mcha","mcki","mcma","mcmg","mcmv","mcnv","mcof","mcpa","mcqn","mcro","mcso","mctk","mctu","mcub","mcuv","mcuw","mcxg","mcxy","mcyo","mdad","mday","mdba","mdek","mdjt","mdkb","mdkt","mdlq","mdne","mdnj","mdod","mdoj","mdsa","mdsc","mdtc","mdwt","mdxx","meaz","meck","medi","mefy","meko","memb","memd","meov","merq","mers","mesj","meuo","mevx","mezd","mezk","mfce","mfho","mfji","mflx","mfnr","mfoc","mfpg","mfrf","mftr","mfug","mfvo","mfxn","mfzj","mfzu","mfzv","mfzx","mgcm","mgdu","mges","mgfc","mggr","mggy","mghv","mgie","mgln","mgve","mgyw","mhbf","mhbu","mhcl","mhem","mhgm","mhgs","mhiq","mhkb","mhkz","mhpb","mhpx","mhrs","mhrw","mhyc","mhyr","mibh","midz","miei","miet","mige","mihy","mijg","miji","mika","mikk","mikx","mioe","mirl","misx","miti","miuc","miun","mivr","miyf","mjcb","mjez","mjff","mjhq","mjjj","mjjr","mjkn","mjlj","mjlr","mjor","mjpa","mjrt","mjuy","mjxr","mjzo","mkbt","mkdv","mkgp","mkiz","mkkh","mkmb","mkmi","mknp","mkpc","mkqq","mkrf","mksa","mksu","mktv","mkud","mkvd","mkwr","mkxh","mlat","mlce","mley","mlfo","mlgc","mlgr","mlhu","mliv","mlkr","mllf","mlli","mlly","mllz","mlqd","mlsw","mlvb","mlvu","mlwv","mlxb","mlye","mmab","mmai","mmbz","mmem","mmfq","mmib","mmlh","mmlo","mmmi","mmqt","mmrx","mmvd","mmzm","mnab","mnaz","mnbl","mnbz","mncc","mngm","mngp","mnli","mnln","mnme","mnnu","mnsd","mnsl","mnut","mnxf","mnyi","mnyv","mnzl","mogu","mois","mooz","mopc","mord","morf","mosk","moyk","mpbr","mpcd","mpfi","mphv","mpjh","mpkh","mply","mpne","mppe","mppv","mprf","mpro","mpvt","mpwe","mpxh","mpyy","mqby","mqco","mqcz","mqep","mqif","mqkx","mqlj","mqmm","mqnn","mqns","mqoy","mqpl","mqqt","mqti","mqvw","mqwb","mqxt","mqxw","mqyi","mrbn","mrem","mrex","mrgq","mrhj","mrio","mrjj","mrmd","mrmh","mrnh","mrot","mrpd","mrpn","mrsl","mrtp","msdf","mses","mske","msko","msmb","msmi","msoj","msrf","msrz","mssc","mtdx","mtea","mtgy","mthj","mtnw","mtog","mtop","mtse","mtwa","mtwu","mtxw","mtzl","mual","muay","mudv","mueh","mufp","mukv","muma","mumk","mumq","muoj","muqr","musn","mutm","muvz","muxg","muyg","muzd","muzg","mvci","mvco","mvdz","mvek","mvgd","mvgl","mviy","mvjp","mvjx","mvms","mvnm","mvoy","mvrk","mvyb","mwap","mwbj","mwdw","mwef","mwei","mwik","mwve","mwwj","mwxo","mwyu","mxai","mxcp","mxeq","mxgg","mxgl","mxhf","mxhi","mxhj","mxiw","mxjk","mxjr","mxkv","mxmf","mxvd","mxvi","mxvs","mxwu","mybs","mycx","myeo","myhi","myni","myri","myrv","mytc","myvq","mywp","myyq","myzq","mzdg","mzfp","mzgy","mzih","mzkj","mzkl","mzth","mztl","mzut","nabp","nadk","nall","nama","namx","naqo","naqw","naqx","narb","natu","nauv","nayg","nbdh","nbdj","nbig","nbim","nbjm","nbmz","nbnc","nbsd","nbvn","nbxi","nbxo","ncbq","ncfd","ncgo","ncgu","ncgx","nchk","ncjp","ncme","ncmh","ncmz","ncng","ncov","ncps","ncsy","nctq","ncuo","ncvc","ncwc","nczz","ndag","ndce","ndej","ndex","ndgc","ndjl","ndkn","ndku","ndlh","ndpm","ndqg","ndte","ndua","nduj","neaw","nede","neeb","nefb","nehe","nejp","nekk","nekq","nekr","nell","nely","nens","neoj","nepo","nerr","neug","nevx","nezm","nfcn","nfcv","nfhn","nfhz","nfkf","nfmo","nfmu","nfoo","nfpb","nfsq","nfws","ngeh","ngey","nghm","ngim","ngit","nglp","ngno","ngpn","ngqi","nguw","ngve","ngvz","ngyz","ngzo","nhaa","nhdv","nhfh","nhfp","nhhf","nhia","nhnk","nhog","nhrk","nhsb","nhvd","nhwf","nhxo","nhxx","nhye","nhyu","nieo","nifj","nifu","nigv","nijq","nild","nipz","nist","niuk","niva","niwn","niwp","nizc","nizw","njbd","njbw","njjb","njoj","njpd","njqn","njtk","njxr","njxx","njzd","njzr","nkbt","nkbz","nkdb","nkew","nkfj","nkgj","nkia","nkle","nknc","nkny","nkou","nksw","nkvk","nkxs","nkyg","nkzu","nlcv","nlfv","nlgg","nljk","nljr","nlny","nluu","nlwb","nlwd","nlwv","nmbu","nmez","nmja","nmki","nmlq","nmnn","nmom","nmpc","nmrl","nmsm","nmtj","nmvn","nmvv","nmwh","nmwx","nmyi","nmym","nnbs","nncz","nndb","nngh","nnqc","nnra","nnwq","nnwr","nnxn","nnya","nnyp","nnzj","nnzp","noax","nocc","nogi","nogm","nohw","noja","nokg","nolg","nomg","noni","nopj","nopn","nost","novk","nowi","noxb","noxu","npbn","npde","npdv","nped","npeo","npeu","nphy","npjs","nppf","nqbh","nqea","nqem","nqey","nqfu","nqgi","nqki","nqko","nqlq","nqma","nqom","nqqz","nqrp","nqsh","nqum","nqwu","nqzj","nral","nram","nrbs","nrce","nrct","nrcz","nrdc","nrfn","nrfv","nrib","nrkf","nrnr","nroo","nror","nrqb","nrry","nrsi","nrst","nrtw","nruc","nrud","nrui","nrve","nrvq","nrzb","nrzh","nsas","nscw","nsdm","nseu","nsfj","nshw","nsis","nsjr","nskn","nsoy","nspy","nsrf","nsvt","nswg","nsys","ntbh","ntea","ntfc","ntii","ntiq","ntjg","ntli","ntlk","ntne","ntop","ntqw","nttn","ntxs","ntxx","nuax","nugl","nugr","nujc","nukm","nuol","nuos","nuqd","nuta","nuuz","nuvx","nuya","nuzf","nvax","nvba","nvbh","nvda","nveh","nvlc","nvld","nvlk","nvnm","nvrj","nvrs","nvto","nvtt","nvtu","nvuz","nvwc","nvxp","nvyy","nvzy","nwcx","nwdd","nwez","nwge","nwgr","nwgw","nwho","nwjm","nwmd","nwne","nwqb","nwri","nwvv","nwyb","nxbb","nxbc","nxbl","nxci","nxdf","nxgs","nxhj","nxib","nxii","nxjq","nxpz","nxut","nxwp","nxyf","nybo","nyec","nyfv","nygx","nyhq","nyje","nymk","nyqb","nyqo","nyqy","nysz","nyvx","nywh","nywx","nyxx","nyyd","nyyu","nzcb","nzdp","nzhn","nzjz","nzlr","nzlw","nzmd","nzpn","nzqr","nzrt","nzsq","nzto","nzwq","nzya","oaba","oabf","oaeb","oagd","oajk","oakx","oamc","oamf","oasf","oavb","oawa","oaxu","oazd","obbb","obcu","obgr","objm","objp","obkf","oblq","obnw","obpb","obrw","obtd","obyw","ocbn","occg","oces","ocgw","ochc","ockd","ockw","oclr","ocme","ocnn","ocor","octm","ocua","ocuu","ocyw","odar","odas","odat","oddg","oddt","odfc","odfr","odix","odod","odsj","odva","odwf","odwl","odxs","odyj","oeab","oecl","oeen","oefs","oehe","oeib","oeiq","oejf","oejj","oeko","oeli","oene","oeod","oerq","oesj","oetv","oetw","oevi","oevq","oevu","oeya","oeyn","oeyo","oeyx","ofhg","ofhl","ofhm","ofjb","ofli","ofmi","ofms","ofpd","ofrh","ofro","ofte","oftr","oftu","ofvc","ofvg","ofyk","ofzy","ogab","ogaj","ogao","ogau","ogdi","ogdx","ogdz","ogeu","oghj","ogkw","ogll","ognj","ogug","ogvc","ogvs","ogws","ogyb","ogzs","ohay","ohdd","ohgo","ohiz","ohkr","ohks","ohky","ohls","ohni","ohph","ohpx","ohxo","oiak","oidi","oifl","oigc","oiik","oiod","oioh","oiol","oiom","oiqh","oiqq","oiqx","oiqy","oiry","oism","oiss","oiti","oivk","oixy","oiyj","ojae","ojbh","ojbj","ojip","ojiw","ojkr","ojku","ojme","ojmx","ojqe","ojrj","ojtf","ojuy","ojvt","ojwu","ojxx","ojyw","okbt","okgh","okhe","okhj","okhz","okic","okkl","okoh","okoo","oksa","okvn","okxs","okzf","okzs","oldd","olez","olgy","olih","olju","olko","olls","olsd","olus","olvf","omae","ombz","omdm","omee","omeo","omfd","omil","omll","omlz","omtx","omwk","omxa","omxj","omyc","omyn","omzd","onaq","onav","onhg","onjs","onjy","onmf","onrk","onzt","onzw","ooad","ooal","ooay","oodf","oonc","oonf","oonq","ooob","oooz","ooqm","ootb","ooth","oowj","ooyf","opbe","opcf","opdm","opdp","opff","opgs","opgx","opjs","opju","opkp","oplr","opon","opqu","optf","opwj","opzw","oqdu","oqec","oqge","oqhe","oqin","oqio","oqjc","oqjg","oqle","oqlt","oqlx","oqml","oqni","oqnx","oqop","oqsc","oqtn","oqwb","oqyf","orbw","orim","orjo","ormg","ormj","orns","orpp","orsr","ortf","orvp","orxz","orzd","oseb","osem","osjk","oskp","oslj","osoy","osqn","ossl","ossp","ostq","osun","osxl","oszi","otae","otbb","otct","otff","otfj","oths","otia","otij","otjq","otkp","otkw","otlb","otlv","otlz","otpf","otqa","otsj","otui","otyc","otyw","ouap","oucp","oucx","oucy","ougv","ounx","oupd","ouqh","ouqk","ouwe","ouxo","ouys","ovbb","ovbd","ovce","ovjd","ovjn","ovly","ovmx","ovnf","ovni","ovnj","ovnk","ovnu","ovol","ovot","ovqc","ovrd","ovrw","ovvn","ovvz","ovxt","ovzp","owae","owdt","owfg","owid","owme","ownu","owpf","owpv","owqu","owqz","owwa","owzc","owzw","oxam","oxbh","oxbz","oxdo","oxhq","oxhs","oxhy","oxib","oxiy","oxky","oxpx","oxrv","oxui","oxuu","oxvi","oxvx","oxzi","oyab","oycl","oyeu","oyfd","oyfg","oyfr","oyhr","oyja","oykx","oyll","oymc","oymf","oynb","oyoq","oypd","oytg","oyth","oytr","oyuy","oyvm","oywo","oyxc","oyxj","ozao","ozcm","ozdb","ozdh","ozfl","ozfx","ozhr","ozky","ozns","ozpj","ozqr","ozsc","ozsh","ozuk","ozup","ozuv","ozvr","ozxo","ozzb","ozzm","paad","paev","pafs","pagy","pahd","paix","pakc","pakk","pamr","panr","paos","papd","pauv","pawo","paxl","payd","pbbu","pbff","pbfp","pbge","pbiu","pblh","pbnh","pbnt","pbrv","pbva","pbxg","pbzc","pcco","pcei","pcfs","pcft","pcgi","pcgj","pchw","pcjq","pcqi","pcsc","pcsy","pcxu","pczb","pdbr","pdeo","pdpt","pdqq","pdso","pdxz","pebz","peci","pect","pedn","peef","pegf","pegk","peie","peix","pejp","peku","pelj","peln","pemc","pemn","penf","penh","peqg","perh","pevj","pexu","pfby","pfdi","pfdv","pfep","pfhg","pfic","pfih","pfmw","pfpb","pfvq","pfxj","pgfg","pggq","pgli","pgpd","pgrh","pgtk","pguv","pgwj","pgxi","pgym","pgyn","pgzi","phad","phdd","phjo","phjz","phkq","phmb","phmi","phqk","phqv","phrq","phrr","phrs","phsf","phss","phsu","phta","phvi","phyx","phzc","phzt","piab","piar","pias","pibj","pift","pifu","pigh","pihp","pija","pijw","pimu","pisj","pitc","pjah","pjdf","pjed","pjfn","pjgb","pjhm","pjhx","pjin","pjiq","pjir","pjmh","pjpc","pjpm","pjqe","pjsa","pjti","pjut","pjvm","pjvt","pjvz","pjwg","pjwv","pkcq","pkcr","pkif","pkiy","pkiz","pkkj","pkkq","pklj","pknx","pkoi","pkpj","pkrn","pkrp","pksr","pktw","pkuf","pkvf","pkxs","plbl","plca","plct","pldq","plgo","pljx","pljz","plob","plok","plpe","plpk","plpn","plrs","plue","plva","plya","plym","pmfa","pmhk","pmif","pmkx","pmmd","pmmp","pmpa","pmst","pmtm","pmvl","pnba","pngn","pnio","pnlr","pnnh","pnrg","pnsd","pnsk","pntl","pntp","pnzl","pnzx","pobq","podk","podv","poeh","poic","pokm","ponk","pooo","porh","potb","ppau","ppbc","ppbk","ppgq","ppjg","ppju","ppms","pppw","ppri","pprp","ppss","ppvc","ppze","pqct","pqea","pqfp","pqgd","pqhx","pqjz","pqkc","pqkq","pqnf","pqos","pqra","pqrs","pquk","pqvd","pqvh","pqvp","prai","prbd","prca","prgf","prgx","prkk","prkx","prla","prna","prok","prpl","prps","prrm","prsn","prst","prtf","prth","prvg","prvi","prwb","psbp","pseg","psgt","psig","psjc","psjz","psps","pspv","psqj","psrr","pssj","pssm","pstf","psuq","psur","psuy","psvc","pswf","psxn","psyo","pszv","ptan","ptbb","ptbc","ptbz","ptcp","ptiz","ptju","ptmy","ptnk","ptpg","pttj","ptvd","ptvg","ptwr","ptzp","pubh","pubq","puco","puiv","pulr","pulv","pumt","pura","purq","purv","pusb","puso","pusw","puyq","pvca","pvdj","pvez","pvhe","pvnb","pvqj","pvte","pvwl","pvyb","pvzb","pwad","pwdh","pwec","pwex","pwio","pwix","pwse","pwtd","pwvy","pwxf","pxei","pxfz","pxjy","pxod","pxpb","pxpt","pxtk","pxtm","pxun","pxvo","pxvp","pyac","pycy","pydq","pyge","pygk","pyhr","pyjv","pylk","pylo","pymv","pyns","pyzc","pzbo","pzdh","pzfq","pzjq","pznl","pzsc","pzsl","pzuo","pzvt","pzwp","pzym","pzyz","qaaf","qagu","qajq","qajx","qalr","qamj","qapf","qaum","qauq","qauy","qavu","qawa","qaxp","qayl","qazk","qbel","qbez","qbim","qbjs","qblc","qbln","qbma","qbmi","qbnx","qbqq","qbrc","qbss","qbts","qbuh","qbvk","qbvu","qcbp","qcbx","qceo","qcff","qchj","qcip","qcir","qcjj","qcmp","qcnh","qcwx","qdei","qdem","qdhw","qdlt","qdqv","qdrm","qdrq","qdtz","qdux","qdvu","qdyy","qdzk","qead","qecl","qegk","qegt","qeii","qeja","qejs","qeku","qend","qeuq","qevk","qeyd","qeyy","qfdp","qffo","qfgg","qfgl","qfie","qfif","qfix","qfjb","qflw","qflx","qfmg","qfnp","qfpc","qfpm","qfqm","qfrr","qfuj","qfvm","qfwy","qfxv","qfzq","qgbo","qgda","qgfl","qgju","qgnf","qgng","qgnr","qgpv","qgul","qgus","qgvl","qgvm","qgxl","qhao","qhba","qhcm","qhfb","qhfi","qhjq","qhmd","qhmx","qhnh","qhnr","qhoz","qhrg","qhvm","qhwk","qhyq","qhzt","qidc","qier","qijf","qijl","qilp","qipw","qipx","qisq","qitj","qiug","qixg","qixs","qiyd","qjbz","qjdp","qjdu","qjkt","qjmd","qjna","qjnv","qjoc","qjuj","qjvx","qjwz","qjyg","qkcl","qkdc","qkeg","qkgj","qkka","qkko","qkob","qkqe","qkqi","qkte","qkth","qktp","qkun","qkus","qkwi","qkyq","qkzd","qlbb","qlcc","qles","qlhj","qlhq","qlib","qlid","qliv","qlma","qlmi","qlpk","qlql","qltt","qmag","qmff","qmmc","qmpv","qmqf","qmqt","qmrg","qmtg","qmts","qmyu","qmzd","qnbt","qndu","qnil","qnjh","qnov","qnta","qnth","qntu","qnyh","qnyu","qnzt","qobq","qocv","qodd","qodi","qofv","qojq","qona","qonj","qooz","qora","qowr","qoye","qoyz","qpdh","qpdo","qpfw","qpgx","qphv","qpig","qpmn","qpni","qpop","qpqs","qpsh","qptl","qpup","qpxk","qpyy","qqay","qqbt","qqea","qqfa","qqgs","qqks","qqmb","qqnq","qqnt","qqog","qqok","qqpe","qqub","qqzr","qqzz","qrav","qrck","qrct","qrfj","qrhv","qrji","qrkp","qrnh","qrnw","qrpg","qrtl","qruf","qruw","qrvq","qrvs","qsbr","qsci","qsdf","qsdh","qsfa","qsfb","qsht","qsij","qslr","qsqf","qsvb","qsvx","qswz","qsxk","qsyv","qsze","qtap","qtcx","qtdm","qtdu","qtel","qtfl","qtgv","qthc","qtiu","qtlh","qtoi","qtte","qttm","qtuk","qtxg","qtxl","quau","qubr","qudc","queh","quer","qufd","quhh","quhi","quje","qujl","qule","qulk","qult","qunm","quoo","quoy","quqm","qurb","qurj","qury","qusz","quwg","quxg","quxj","qvca","qvdm","qveq","qvfc","qvfx","qvgp","qvhh","qvlj","qvlk","qvlq","qvmz","qvsz","qvtg","qvve","qvvq","qvvz","qvyh","qvzf","qwbd","qwbv","qwck","qwda","qwdr","qwfm","qwhw","qwoc","qwoq","qwqd","qwve","qxbh","qxbj","qxcs","qxcw","qxed","qxhg","qxjd","qxoa","qxot","qxpo","qxqr","qxrn","qxuy","qxve","qxxv","qyjr","qykv","qylk","qymn","qyqv","qyvk","qywk","qzaa","qzdk","qzgf","qzgx","qzik","qzmr","qznr","qzou","qzsf","qzsr","qztw","qzue","qzwa","qzwy","qzyr","qzzv","raat","rabi","racm","rago","rahr","rait","ramg","ranb","raom","rapz","rats","raue","raxm","rayp","rayq","rbal","rbaw","rbbk","rbcz","rbdr","rbeg","rbfx","rbgs","rbjh","rbjn","rbjw","rbqe","rbqx","rbtc","rbvv","rbxm","rbxp","rbxv","rbya","rbzy","rcae","rcay","rccf","rcfa","rcfd","rcin","rcjx","rcmv","rcoq","rcqx","rcts","rcvb","rcvn","rcwm","rcxs","rcyn","rczw","rczy","rdae","rdar","rdcf","rdcn","rdec","rdgd","rdhz","rdjm","rdjr","rdjt","rdjz","rdme","rdnz","rdoc","rdow","rdqg","rdrc","rdtz","rdue","rduf","rduh","rdva","rdzw","rdzy","reaz","rebb","rebl","redn","refp","regw","rems","reoc","repk","rete","retr","reyb","reyd","reyi","reyx","rfac","rffp","rfgo","rfkk","rfqk","rfql","rfsk","rftc","rftj","rftn","rfud","rfvv","rfxp","rfyc","rfzx","rgbb","rgdh","rgdo","rgfn","rgnc","rgof","rgpj","rgrn","rgsb","rgsn","rgso","rgtr","rgtv","rgui","rgva","rhac","rhbw","rhca","rhcf","rheh","rhel","rhjf","rhkb","rhkm","rhmc","rhmp","rhqv","rhqz","rhvu","rhxy","rhya","rhyk","ribw","ricp","rifn","rihq","rihz","rijr","rikf","rimj","rioz","ripl","rirx","riuj","rixp","riyq","rizr","rjao","rjap","rjgi","rjio","rjmg","rjrg","rjsp","rjva","rjwq","rjyi","rkcb","rkcc","rkcd","rkcv","rkhr","rkib","rklw","rkmo","rkmy","rkoz","rkph","rkqp","rktc","rkuh","rkve","rkxx","rlae","rlaq","rlfg","rlit","rllb","rlny","rlrl","rlsv","rltf","rlun","rluu","rlvn","rlyr","rmav","rmgf","rmiy","rmla","rmmj","rmpb","rmps","rmqi","rmqq","rmrn","rnbn","rncx","rndw","rnmv","rnof","rnpk","rnqe","rnra","rnri","rnrp","rnst","rntr","rntv","rnuf","rnuo","rnvb","rnzo","rofn","rohk","rohx","roix","ropc","rosk","rouo","rovo","roxx","rpbo","rpdq","rpmf","rpnq","rpoj","rpor","rpsc","rpsw","rpsx","rpta","rpux","rpva","rpxt","rpzq","rqav","rqcg","rqlx","rqlz","rqok","rqqb","rqvb","rqwv","rqyp","rqze","rrbq","rrex","rrib","rrih","rrkr","rrnr","rrok","rrpr","rrst","rrvh","rrzw","rsai","rsef","rsfj","rsgc","rsgn","rshl","rsjs","rskr","rspa","rssr","rsuq","rsuv","rsvj","rtcs","rtdj","rtee","rteh","rtfg","rtgg","rthu","rtii","rtil","rtkj","rttg","rtwo","rtws","rtwv","rtyx","rubd","rudu","rufx","ruhe","ruht","rulg","rumg","rumq","ruqw","rurj","rute","ruty","ruue","ruvt","ruwg","rvac","rvcq","rvem","rveu","rvif","rvle","rvln","rvlu","rvod","rvpo","rvpy","rvrp","rvsj","rvsn","rvti","rvtk","rvxo","rwdz","rwez","rwft","rwik","rwkk","rwlk","rwoo","rwpd","rwtc","rwuy","rwvx","rwwk","rwwn","rwxu","rwym","rxbl","rxbq","rxbr","rxct","rxez","rxfh","rxga","rxgm","rxpg","rxqm","rxqq","rxta","rxte","rxul","rxvu","rxxp","rxzr","ryad","rybw","ryci","rycl","rydw","ryes","ryhv","ryio","ryiv","ryjv","rykk","rykw","rylr","rymh","rypn","ryqh","ryqx","ryth","rytu","ryvv","rywl","rywo","rywx","ryyl","ryyq","ryzz","rzap","rzaq","rzba","rzbp","rzct","rzdi","rzif","rzii","rzij","rzip","rzit","rzjd","rzkl","rzoy","rzqo","rzub","rzwf","rzwi","rzxo","rzzg","saev","sakw","sanl","sapk","saqw","saqx","saxl","sayl","sbah","sbck","sbco","sbfs","sbiw","sbiz","sbjm","sbor","sbpy","sbul","sbux","sbvs","sbzj","sbzl","scah","scai","scas","scem","scfx","scgl","schr","schy","scjr","scjz","scle","scvf","scvn","scwd","scxb","sdas","sddw","sdec","sdkd","sdng","sdno","sdnr","sdnt","sdoh","sdpu","sdrq","sdze","seda","sedh","sedv","segn","sehg","sehi","seib","seix","sekk","semr","seno","sesh","sfax","sfdw","sffc","sffn","sfhz","sfkh","sfkp","sfmf","sfqj","sfql","sfsl","sfsm","sfue","sfur","sfuy","sfvx","sfwl","sfxr","sgab","sgbe","sgjd","sgmj","sgml","sgmz","sgnv","sgpc","sgpo","sgrz","sgtb","sguk","sgvg","sgvp","sgwr","sgws","sgwt","sgyz","sgzv","shdu","sheh","shia","shjb","shjc","shln","shmq","shmz","shon","shqn","shrx","shsv","shta","shum","shwu","shxi","shzv","sidk","sidl","sidx","sifl","sihf","siiq","sinq","siqj","siql","siru","sivk","siyk","siyt","sizp","sjcy","sjds","sjea","sjhg","sjjy","sjlt","sjmd","sjns","sjql","sjtj","sjui","sjuo","sjuv","sjwt","sjxu","sjye","skbh","skcd","skdo","skdx","sket","skfg","skjj","skpf","sktd","skus","skvw","skwm","skzp","slal","slan","slcd","sldd","slgf","slio","slox","slqn","slqo","slqs","slrr","slsr","slxf","slym","slyr","smbp","smdv","smel","smfu","smhm","smid","smin","smlh","smlz","smob","smoq","smpn","smpv","smqa","smtg","smwu","smzl","sncm","snfi","snhl","snkc","snlx","snnl","snnm","snny","snqq","snqs","snsm","snsv","snue","snwi","snwm","snyo","snyu","snyz","soag","soas","sobb","soet","sohq","soij","sojh","sonw","sopi","soqs","sork","sowj","soxc","sozy","spap","spbm","spdc","spdw","spev","spfr","splj","spmz","spra","spsn","spsz","sptp","spuu","spvn","sqax","sqay","sqdg","sqeh","sqjx","sqou","sqqq","sqrd","sqsm","sqzf","srak","sram","srbh","srcw","srda","srhv","srjb","srlo","srls","sros","srpe","srxc","sryt","srzu","sscg","sscu","ssfn","ssfr","ssgj","ssmh","ssmt","ssol","ssse","sstj","ssuj","sszm","stau","stbv","stfh","sthq","sthx","stjz","stwb","stxn","subi","sudx","sudz","sufu","sugq","suiw","sujf","sumi","summ","sumo","sunx","supq","suvq","suxo","svas","svbk","svcd","svgr","svie","sviu","svjm","svjx","svna","svpq","svqc","svrt","svtf","svwb","swbd","swqv","swrx","swsw","swte","swvf","swwf","swxg","sxak","sxam","sxbg","sxdr","sxiw","sxkp","sxmq","sxqb","sxun","sxuo","sxxz","sxzf","sxzr","syat","sybh","syct","syfq","syfv","sygl","syid","syir","synq","syoj","syou","sypc","syqf","syqp","syra","syrf","syue","sywg","syxy","syzu","szam","szdc","szee","szgb","szlv","szmc","szod","szoj","szqj","tage","tagy","taie","taiq","tajg","tako","tamk","tapj","tapv","tast","tasw","tatt","tayw","tayx","tazt","tbcv","tbhg","tbjc","tbjm","tbkf","tblh","tbnn","tbqq","tbsa","tbtj","tbtr","tbwj","tbwy","tbxa","tbxp","tbyc","tcaf","tcao","tcbj","tcbn","tcbo","tcen","tcgg","tchc","tchv","tcji","tckc","tcms","tcrl","tcsc","tcvi","tcwp","tcyn","tcyp","tdat","tdbp","tdde","tdfj","tdfw","tdiv","tdji","tdkk","tdlc","tdld","tdlr","tdmt","tdod","tdoy","tdsf","tdvp","tdvt","tdyf","teab","tebc","tecc","tefw","teid","tejo","tejs","temf","tepd","teqq","teqy","teue","teuu","tewc","tewu","tewz","texb","tfbn","tfcy","tfew","tffu","tfmo","tfpb","tfpm","tfqy","tfrw","tfry","tfzi","tfzo","tgfo","tgjo","tgkv","tgqm","tgss","tguz","tgws","tgxb","tgxv","tgyg","tgyv","tgzy","thch","thdb","thdu","thet","thfn","thgy","thim","thkh","thmg","thpx","thsv","thtk","thtw","thua","thvt","thwy","tiau","ticw","tiez","tifa","tigg","tiiz","tije","tikz","tipc","tirm","tirs","tirw","tisu","tith","tity","tiuk","tivv","tiwy","tixm","tjag","tjbs","tjbt","tjcp","tjey","tjhb","tjiu","tjjh","tjjl","tjll","tjly","tjqx","tjtp","tjuh","tkbc","tkic","tkid","tkkx","tkli","tklp","tkpe","tkqp","tkrf","tkrl","tkrw","tkyx","tkzg","tlcz","tldf","tldm","tlgh","tlke","tlmi","tlnr","tlqn","tlry","tluc","tlvn","tmco","tmee","tmfh","tmjt","tmkm","tmkn","tmlk","tmmi","tmmr","tmno","tmqh","tmqm","tmro","tmry","tmsh","tmvn","tmxi","tmxj","tmxx","tmyj","tnad","tncj","tndi","tnmb","tnpn","tnrw","tnxl","tnyq","toes","tolr","toma","topg","torr","toul","toyh","toys","tpan","tpau","tpbe","tpdx","tpes","tpfr","tpgp","tphc","tphg","tphv","tpip","tpkp","tpmp","tpow","tpoy","tppm","tppo","tpps","tpqp","tpsy","tptf","tpto","tpuc","tpza","tpzk","tqbf","tqbq","tqbz","tqcf","tqdi","tqhw","tqlo","tqlv","tqrg","tqrk","tqrz","tqtz","tquw","tqyd","tqza","trai","trdp","trge","trgk","trjo","trjs","trma","trmh","trmn","troo","trry","trsw","trtp","truj","truq","trux","truz","trvz","trwo","trxg","trxw","tscy","tseg","tsfa","tsfh","tsfq","tsif","tsjj","tskb","tsmx","tsno","tsom","tsor","tsql","tsuj","tsum","tsux","tsvz","tswu","tswz","ttbv","ttef","tthw","ttji","ttkn","ttlj","ttnv","ttoe","ttor","ttsi","ttsk","ttsp","ttya","ttyc","ttyr","tubo","tubp","tufc","tufk","tufp","tugp","tugu","tuhx","tuic","tujl","tukz","tunb","tunz","tuph","tuvf","tuvm","tuvn","tvap","tvcr","tver","tvez","tvhb","tvko","tvng","tvqd","tvqp","tvub","tvud","tvuu","tvwq","tvxc","tvxn","tvxt","tvyf","twbj","twbq","twcr","twou","twpc","twqs","twsj","twso","twwv","twwy","twxi","txbe","txfk","txhc","txir","txjv","txkz","txlg","txnl","txnn","txpd","txqn","txrj","txuv","txvw","tyau","tyct","tydl","tyeh","tyfu","tygb","tyjy","tykd","tync","tyqq","tyrv","tyui","tyur","tyvm","tyvs","tyxh","tyyh","tzae","tzax","tzby","tzcn","tzcs","tzef","tzgi","tzgv","tzhb","tzhd","tzii","tzjj","tzjl","tzkc","tzoa","tzou","tzqp","tzrh","tztu","tztw","tzvg","tzvo","uabi","uabn","uafd","uagi","uaha","uahd","uahk","uajr","uals","uaml","uamv","uanj","uaog","uaqb","uask","uawv","uaxo","uazb","ubab","ubaz","ubcu","ubhe","ubhr","ubib","ubis","ubkb","ubkk","ubkw","ubno","ubsq","ubue","ubxi","ubxu","ucaz","ucbg","uchf","ucjc","ucjn","uclg","ucnx","ucok","ucov","ucqs","ucrw","ucwv","ucxx","ucym","ucyu","udad","udbo","udbs","udcm","uddz","uden","udev","udff","udfo","udfx","udgp","udit","udos","udqd","udqh","udtb","udtz","udud","udvq","udwn","udwt","uebp","uedz","uefp","ueia","uejf","uejj","uepy","uetf","ueuj","uexr","ufaf","ufay","ufdr","uffq","uffv","ufhg","ufkf","ufls","uflz","ufnr","ufpg","ufpw","ufsr","ufsz","ufur","ufvq","ufww","ufyn","ufza","ugar","ugav","ugco","uggs","ughh","ugmq","ugnp","ugnz","ugot","ugtf","uguc","ugxd","ugyu","ugzo","uhbs","uhbw","uhcs","uhcv","uhkj","uhkq","uhpg","uhpi","uhql","uhqn","uhsf","uhvj","uiaq","uics","uidc","uigf","uili","uilz","uind","uitr","uiux","uivs","uizj","ujar","ujcb","ujcv","ujcz","ujez","ujfm","ujkq","ujky","ujod","ujur","ujut","ujwa","ujwh","ujxd","ujyn","ukaa","ukah","ukas","ukfr","ukfv","ukgm","ukhy","ukjc","uknw","ukpg","ukre","ukwk","ukxm","ukxv","ukzu","ulal","ulbb","ulbc","ulbr","uldj","ulha","ulhq","ulmm","ulok","ulom","ulpg","ulvx","ulwh","ulws","umbf","umbz","umem","umfi","umga","umgh","umjv","umlj","umlk","umlr","ummu","umom","umxm","unab","unac","unar","uncu","unev","unfk","unfn","unmz","unne","unox","unph","unrl","unsd","unss","unvg","unvt","unxr","uoam","uodf","uoho","uokq","uomf","uomp","uood","uopm","uoql","uotg","upar","upep","upfi","upke","upkt","upln","uppl","upqi","upry","upst","uptt","upxf","upxs","upzp","uqej","uqel","uqgr","uqgz","uqib","uqik","uqjf","uqkh","uqmc","uqok","uqot","uqvf","uqvj","uqxn","uqyn","uqyu","uqyw","urak","urce","urhb","urnh","uroy","uroz","urpx","ursd","ursn","urtl","uruf","urui","uruj","urwj","usbr","usey","usgb","usgp","usgz","usjo","uslm","uslu","usmb","usmp","usog","usvm","usyd","uszz","utdu","utef","utie","utjg","utjp","utlx","utow","utpc","utsb","utse","utsh","utui","utvj","utwh","utzc","utzv","uuak","uucz","uudl","uuhj","uuia","uujs","uumr","uupi","uupm","uuti","uutl","uuur","uuzi","uuzp","uvaj","uvbf","uvcx","uvdj","uveq","uvet","uvfg","uvfx","uvhu","uvjp","uvok","uvpn","uvsl","uvsr","uvuv","uvvg","uvwf","uvww","uwbx","uwcn","uwdg","uwdv","uwff","uwhu","uwjq","uwkl","uwmr","uwny","uwsj","uwte","uwul","uwuz","uwvh","uwvv","uwxn","uxce","uxdo","uxel","uxez","uxft","uxio","uxir","uxjs","uxjz","uxmo","uxoj","uxrc","uxsv","uxyo","uxyt","uyah","uyav","uyaz","uycg","uydt","uyge","uygj","uyhf","uyia","uyoj","uyrm","uyuh","uyxf","uyyi","uzac","uzap","uzbn","uzcp","uzcr","uzdz","uzhg","uzik","uzlh","uzrb","uzvr","uzxm","uzxn","uzyv","vabs","vagk","vagn","vajl","varo","vars","vauo","vawa","vbam","vban","vbay","vbdl","vbdu","vbfq","vbki","vbps","vbtz","vbvv","vbwd","vbwn","vbxs","vbxy","vbzn","vcbg","vcbj","vcfs","vcfw","vcha","vcju","vclh","vcmx","vcnd","vcob","vcps","vcsl","vctd","vcvf","vcwm","vcyj","vcyv","vdag","vdbl","vdcc","vddp","vdkg","vdlq","vdnc","vdsg","vdsv","vdul","vdvl","vdvz","vdxe","vdze","vecd","vedb","vego","vegr","vegv","vekk","velh","veoa","veqq","verf","veuv","vewq","vexj","veye","vfag","vfdu","vffb","vffl","vfgt","vfiz","vfnd","vfob","vfpu","vfqp","vfrg","vfrh","vfts","vfvj","vfyc","vgbq","vgkq","vgkr","vgnl","vgqp","vgsv","vgsw","vgvx","vgzb","vgzc","vhbt","vhbv","vhdr","vhfi","vhfs","vhgn","vhkp","vhtc","vhwe","vhyn","vibo","vidm","vije","vijq","vind","viok","vioo","vitf","viwu","vixh","vixt","viya","vjbg","vjdc","vjdn","vjdw","vjdz","vjhg","vjhm","vjiu","vjjo","vjkc","vjmk","vjng","vjou","vjoy","vjpe","vjqn","vjrc","vjrr","vjsi","vjsx","vjxt","vkaa","vkcc","vkdn","vkft","vkid","vkmm","vknq","vkqx","vksd","vkxv","vkyj","vlcy","vlda","vleo","vlid","vlku","vllu","vlmi","vlmj","vltc","vltn","vltq","vlwd","vlyj","vlzo","vlzp","vmas","vmfb","vmfr","vmhr","vmhu","vmlh","vmly","vmnb","vmnq","vmra","vmtz","vmui","vmyv","vmyw","vnai","vnas","vnfn","vnhc","vnhk","vnhp","vnke","vnmu","vnqb","vnqo","vntk","vnwx","vnxa","vnyk","vobo","voby","vocc","voco","vocu","vofi","vofo","voga","voif","vojp","vokw","vomt","vopn","voqt","vory","votf","voua","vouc","vowg","vozx","vpan","vpcw","vpev","vpkg","vple","vpnz","vpoo","vpqw","vprh","vptm","vpvq","vpwg","vpzp","vqcj","vqeb","vqfc","vqgt","vqkk","vqmq","vqnv","vqoy","vqpm","vqpp","vqug","vqva","vqvw","vqww","vrba","vrbw","vrev","vrfu","vrfx","vrhy","vrna","vror","vrpw","vrrq","vrrs","vrvb","vrxa","vsca","vsel","vsfn","vsfr","vshi","vsjf","vsji","vsli","vslr","vspa","vsql","vsrr","vsrz","vssm","vsvl","vsvw","vtcl","vtcv","vtcy","vtet","vtja","vtjh","vtxi","vuab","vuat","vubq","vuco","vuew","vufr","vuik","vujq","vula","vumv","vuqs","vurr","vurt","vuwf","vuwr","vuwu","vuyh","vuyl","vvgu","vvhc","vvhq","vvik","vvkz","vvme","vvoz","vvqh","vvul","vvur","vvzt","vwah","vwbt","vwcm","vwdl","vwgr","vwgt","vwnj","vwoe","vwpy","vwqx","vwrs","vwuo","vxar","vxbg","vxee","vxeu","vxfr","vxfv","vxgx","vxhk","vxhm","vxkm","vxmn","vxpx","vxra","vxtb","vxwp","vxxi","vxyb","vxza","vybs","vycm","vyda","vygl","vyhl","vyij","vyjh","vyjr","vyof","vyon","vysg","vytm","vytu","vyun","vyzn","vzag","vzbi","vzbv","vzcf","vzfe","vzgv","vztz","vzuq","vzyg","vzza","wadc","wadw","wafs","wagr","waht","waju","waqk","wara","wati","watz","waum","wawe","waxe","waxq","waxs","wazo","wbaa","wbaj","wbav","wbay","wbba","wbca","wbcm","wbcr","wbcs","wbeq","wbfw","wbjb","wbjq","wbkj","wbmc","wbmj","wbow","wbvr","wcak","wcau","wcbg","wccl","wcea","wchf","wcja","wckj","wckm","wclv","wcnj","wcqr","wcsc","wcsp","wcxn","wcym","wcyp","wdan","wdbj","wdct","wdcu","wdda","wddt","wddy","wdgv","wdjx","wdor","wdph","wdpr","wdqz","wdux","wdzp","weco","wefz","weiu","wekp","welg","welj","wemm","wemy","weph","weqg","wesd","wexn","weyy","wffj","wfhf","wfhl","wfjz","wfna","wfne","wfnm","wfqp","wfqw","wfrg","wfrn","wfro","wfrr","wfsn","wfvg","wgci","wgdk","wger","wghd","wgja","wgjf","wgkv","wgky","wglw","wgng","wgte","wgvf","wgvr","wgwt","wgxl","wgyk","wgyp","wgzh","whep","whex","whff","whmh","whog","whse","whsf","whtq","whuk","whvf","whvk","whvp","whwi","whxi","whyl","wich","wicj","wicp","widd","widr","wiey","wiga","wihw","wiil","wiiy","wijs","wimg","wiru","wisi","wisk","witr","wiyb","wjbj","wjcp","wjdb","wjga","wjjc","wjml","wjrp","wjsq","wjti","wjtp","wjtu","wjuo","wjvi","wjwi","wjwz","wjyu","wjzm","wkbt","wkdv","wken","wkhp","wkii","wklw","wknd","wkpu","wkss","wkud","wkvf","wkwk","wkwm","wkyt","wkzg","wkzx","wlaw","wlcx","wleb","wlha","wlhb","wlhy","wljj","wlkz","wlnc","wlno","wlnr","wloz","wlqd","wlvq","wlwb","wlxl","wlya","wlyl","wlzz","wmbj","wmdi","wmew","wmfz","wmkj","wmmc","wmrd","wmru","wmsv","wmta","wmvd","wmvo","wmzx","wnfr","wngi","wngx","wnie","wnls","wnnp","wnny","wnqm","wntw","wnvd","wnwz","wnxe","wnyj","woei","wofe","wogk","woie","woio","woma","wooa","woor","woqt","wour","wpci","wpdl","wpgd","wpht","wphv","wpji","wpko","wplw","wpmy","wpnd","wpqk","wpqq","wpug","wpwc","wpxr","wqaj","wqav","wqba","wqcv","wqon","wqsi","wqyu","wqzh","wral","wrbe","wrjr","wrpr","wrqj","wrxq","wryi","wryw","wscs","wset","wsfc","wsfq","wsgx","wshh","wshr","wsks","wsky","wssb","wssk","wsst","wssx","wsuf","wsul","wsuv","wswb","wtdo","wtey","wtgq","wtiy","wtmb","wtmf","wtmt","wtrp","wttg","wuac","wubn","wubx","wueh","wuie","wujk","wukr","wuku","wukz","wulv","wumw","wuny","wusx","wuwh","wuzk","wuzp","wvay","wvek","wvid","wvij","wviu","wvjv","wvlh","wvmj","wvmp","wvny","wvpx","wvqv","wvtz","wvxp","wwao","wwas","wwjs","wwkm","wwku","wwkz","wwlp","wwlx","wwmf","wwnf","wwnl","wwsu","wwxm","wwxu","wwzs","wxbf","wxes","wxfc","wxfw","wxfx","wxgt","wxir","wxky","wxlq","wxnj","wxpg","wxpk","wxpl","wxri","wxtu","wxud","wxvv","wxxc","wxyb","wxyw","wybj","wybm","wydk","wygd","wyhz","wylz","wymr","wypv","wyul","wyvj","wzau","wzbw","wzcb","wzdw","wzep","wzge","wzhk","wzjw","wzof","wzpf","wztm","wztr","wzuv","wzzo","xadu","xafc","xagt","xahw","xakd","xakj","xarh","xaui","xbaj","xbcr","xber","xbkt","xblq","xbmm","xbnp","xboe","xbof","xbpg","xbsm","xbsw","xbtc","xbug","xbum","xbzg","xcba","xccg","xcck","xcgj","xchn","xckg","xctm","xcuo","xcvv","xcxl","xcyc","xczr","xdav","xdaz","xddh","xddr","xdeq","xdjy","xdms","xdnk","xdno","xdpk","xdpr","xdqo","xdum","xdxn","xdzd","xech","xecr","xegr","xegt","xehp","xehy","xejc","xely","xemt","xeoc","xepr","xerh","xerl","xesf","xeub","xewt","xeyn","xezz","xffj","xfgm","xfnh","xfpn","xfvx","xfxo","xgat","xgcf","xgcy","xgez","xgfm","xgia","xgiu","xgjp","xgkg","xgoa","xgqe","xgqp","xgrk","xgse","xgsq","xguy","xgvp","xgye","xgyj","xhaa","xhbh","xhda","xhdm","xheg","xhgk","xhir","xhjd","xhry","xhsa","xhvf","xhza","xibb","xico","xicy","xifd","xifp","xihe","xiip","xiku","xiny","xiok","xipd","xite","xitg","xith","xiuc","xivt","xjbj","xjdu","xjdv","xjdy","xjhg","xjii","xjjn","xjkh","xjko","xjni","xjqa","xjtc","xjvb","xjvn","xjvt","xjvu","xkcm","xkdn","xkff","xkic","xkmq","xkmw","xkmx","xkot","xkqa","xkqe","xkqo","xksy","xkus","xkyg","xkzi","xkzu","xlhp","xljf","xlkq","xlmc","xlmt","xlnl","xlnn","xlps","xlru","xluc","xlvy","xlyu","xmad","xmdw","xmgi","xmgq","xmjo","xmlu","xmmt","xmmz","xmnq","xmor","xmow","xmsb","xmxq","xmze","xnao","xnco","xnfk","xngb","xnhu","xnid","xnik","xnjo","xnow","xnoz","xnpb","xnqn","xnqs","xnqy","xnrq","xnso","xnui","xnvg","xnwl","xnwp","xnwt","xnwz","xnyq","xoac","xoaf","xocd","xocu","xodf","xoel","xoep","xoev","xofo","xoia","xois","xolz","xopx","xopz","xoqp","xoty","xovq","xoyc","xpaz","xpbp","xpbq","xpei","xpfo","xpgk","xppu","xpru","xptr","xpvs","xpxy","xpyr","xpzm","xqfw","xqhe","xqmv","xqnv","xqok","xqol","xqri","xqrm","xqrx","xqrz","xqsg","xqsh","xqud","xquf","xqun","xquv","xqvc","xrcq","xrcy","xrei","xrfk","xrft","xrgg","xriq","xrkx","xrlp","xrma","xrmw","xrnh","xroo","xrsc","xrse","xrut","xrwz","xrxm","xrxn","xryn","xsda","xsdm","xsdo","xsef","xsgd","xsgq","xsjz","xsly","xsox","xspk","xsru","xsrv","xsup","xsxv","xtbm","xtct","xtem","xtfi","xtia","xtli","xtlk","xtoi","xtro","xtrq","xtsw","xttn","xttz","xtvp","xtyg","xtzr","xtzs","xuah","xufd","xuge","xuje","xuju","xuko","xuku","xuly","xumc","xuqz","xurn","xusj","xuxy","xuyb","xuyn","xuzq","xvbj","xvbz","xvez","xvgk","xvhk","xvip","xvjc","xvkf","xvki","xvlm","xvmu","xvou","xvqe","xvso","xvvs","xvyu","xvzw","xwco","xwdg","xwfd","xwic","xwjc","xwjh","xwmw","xwnh","xwnu","xwnz","xwog","xwot","xwox","xwpm","xwrg","xwri","xwsx","xwtk","xwue","xwvx","xwxq","xwyp","xxaj","xxas","xxce","xxhn","xxhx","xxif","xxke","xxla","xxqi","xxuc","xxuv","xxvo","xxzu","xyaa","xyap","xycl","xycr","xyge","xyjc","xylh","xylp","xyoq","xypa","xyrl","xyrv","xysg","xysn","xywf","xyyk","xyzr","xzap","xzhe","xzkd","xzlv","xzlw","xzmj","xzpq","xzse","xztd","xztr","xzud","xzui","xzum","xzup","xzuq","xzzg","yabf","yafl","yahd","yahp","yahx","yaii","yais","yajd","yakq","yakx","yaof","yaqc","yaqr","yaqs","yaua","yauw","yavr","yavz","yawf","yazc","ybbu","ybcs","ybdp","ybhf","ybhq","ybni","ybom","ybox","ybwe","ybwg","ybxe","ybxn","ybyg","ybzj","ycca","yccs","ycdc","ycel","ycgi","ycjx","yckl","yclp","ycnc","ycnk","ycoz","ycqm","ycqq","ycuh","ycya","ycyr","ydec","ydei","ydew","ydhn","ydhp","ydjg","ydnu","ydpf","ydpy","ydqg","ydrh","ydto","yduc","ydui","ydul","yduy","ydvm","yedm","yegb","yeha","yekh","yenm","yesv","yeux","yfaz","yffx","yfgr","yfhy","yfju","yfpy","yfqg","yfql","yfsi","yfst","yfta","yfwb","yfyv","yfzi","yggn","ygim","yglq","ygny","ygoa","ygpk","ygrd","ygtr","ygus","ygwk","ygym","yhbl","yhcs","yhew","yhkr","yhnu","yhpl","yhre","yhry","yhsy","yhue","yhxt","yhxx","yhze","yhzp","yiah","yiat","yiaz","yibg","yibs","yieh","yifi","yigj","yiqq","yirh","yivl","yiwc","yixz","yizn","yizv","yjau","yjdf","yjjq","yjjy","yjol","yjpn","yjpx","yjri","yjud","yjuz","yjwv","yjye","ykan","ykde","ykdu","ykel","ykhc","yknk","ykri","ykrp","yksh","ykui","ykvr","ykxl","ykye","ykzl","ykzu","ylfz","ylgc","ylhb","ylih","yliu","yliw","ylki","ylks","ylpf","ylqk","ylqs","yltp","ylwj","ylzv","ymcg","ymco","ymdr","ymga","ymii","ymjj","ymlp","ymlz","ymma","ymne","ymoo","ymoz","ymqp","ymrg","ymso","ymtn","ymvf","ynba","ynco","yndz","ynei","ynfs","ynhg","ynig","ynis","ynjn","ynkr","ynqh","ynqr","ynxn","ynxs","yoaq","yoct","yoiw","yokt","yone","yosm","youe","yowx","ypak","ypdz","ypgo","yprm","ypro","ypue","yqax","yqcl","yqdf","yqea","yqfu","yqfy","yqjo","yqku","yqlh","yqnn","yqon","yqpx","yqqx","yqsx","yqub","yqup","yqvg","yqvu","yqyn","yqyp","yrdh","yrfa","yrfq","yrfu","yrhl","yrkq","yrqp","yrtd","yrte","yrtu","yrwm","yrwy","yrxo","yscr","ysdq","ysel","ysgb","yshj","ysji","ysjv","yskj","ysph","ysrx","ysut","ysyb","ysyh","yszg","yszr","ytam","ytap","ytbe","ytbk","ytbn","ytda","ytdj","ytdr","ytdy","yteu","ytfx","ytjc","ytjt","ytlm","ytnd","ytpu","ytqb","ytrg","ytzt","yufj","yujs","yuka","yuop","yupc","yuqj","yuud","yuwf","yuxj","yvab","yvbi","yvee","yveo","yvhm","yvhu","yvim","yvpg","yvpu","yvse","yvso","yvus","yvxa","yvxb","yvxg","yvxl","yvyz","ywaj","ywam","ywdr","ywea","ywfc","ywfj","ywkj","ywly","ywnq","ywnx","ywny","ywpd","ywpf","ywpk","ywpq","ywqc","ywqo","ywtu","ywwc","ywxr","ywyd","ywyw","yxaz","yxdo","yxki","yxlw","yxmu","yxob","yxom","yxoq","yxpn","yxvk","yxvn","yxvz","yxws","yxxi","yxzb","yxzd","yyfk","yygr","yyij","yylj","yynf","yyoe","yyqr","yyqs","yyrb","yyss","yyue","yyxb","yyyd","yyzo","yyzu","yzgd","yzja","yzof","yzpa","yzqg","yzrd","yzrt","yzru","yzvs","yzwn","yzwu","yzyj","yzzz","zagv","zaif","zaim","zaki","zale","zalj","zamm","zaoy","zapd","zbaj","zbba","zbbv","zbci","zbdk","zbjs","zbms","zbmy","zbnq","zbrw","zbsk","zbue","zbwu","zbxb","zbzg","zcfd","zchv","zclx","zcmg","zcnh","zcny","zcpf","zcrw","zcsm","zcsz","zcuq","zcvb","zcxk","zdab","zdaq","zdcl","zddi","zdhv","zdkd","zdlg","zdmd","zdph","zdtn","zduq","zdyb","zeeu","zefn","zehr","zehz","zeie","zeju","zeph","zern","zerx","zesr","zeuy","zevc","zezr","zfab","zfci","zfdj","zffy","zfhv","zfiv","zfjf","zfjg","zfkq","zflu","zfmd","zfqd","zfri","zfuq","zfvi","zfzf","zgca","zgjg","zgna","zgnm","zgpk","zgpo","zgqz","zguh","zgur","zgvr","zgzj","zhak","zham","zhbm","zhed","zhfq","zhmi","zhpy","zhso","zhsr","zhxv","zhxx","zhyd","ziar","zibm","zicj","zify","zihf","zija","zikb","zikl","zilh","zimw","zinb","zins","zioi","ziow","zipf","ziqe","ziqn","zise","zism","ziyr","zjeg","zjgk","zjhd","zjin","zjjm","zjka","zjkh","zjmy","zjnj","zjpe","zjqv","zjry","zjts","zjuh","zjya","zkas","zkfj","zkfq","zkhc","zkjl","zkml","zkmr","zkmw","zknq","zknt","zkqi","zkta","zkul","zkvp","zkyw","zkzh","zkzr","zlhc","zlhl","zlko","zlqx","zltv","zlul","zlwd","zlwe","zlxs","zmad","zmaq","zmbd","zmea","zmkf","zmkn","zmko","zmqu","zmrm","zmuf","zmvb","zmvf","zmvs","zmwr","zmxa","zmzr","znbr","znbw","znbz","zncu","zndf","znem","znij","zniv","znjy","znls","znlz","znoz","znqi","znrp","znsp","znvq","znyx","zoel","zoen","zohx","zoim","zokg","zoky","zomv","zonn","zora","zosu","zoth","zous","zovc","zovx","zowv","zpaw","zpbc","zpbj","zpdt","zpge","zplc","zpmh","zpnt","zppm","zprd","zpti","zpwg","zpwz","zqal","zqch","zqco","zqfp","zqjz","zqml","zqof","zqqc","zqsb","zqsz","zqwg","zqxn","zqzx","zqzy","zrbp","zrde","zree","zrfa","zrgb","zrgl","zrgt","zrnh","zrok","zroq","zrqh","zrqo","zryq","zryy","zsdv","zsem","zsen","zshp","zsjm","zskd","zsmk","zsml","zsrv","zsuc","zsuk","zsvk","zsww","zsxd","zszs","zszu","ztay","ztbv","ztcb","ztgl","ztgo","ztnd","ztop","ztot","ztps","zttk","ztuh","ztuw","ztuz","ztvg","ztvz","ztxc","ztxs","ztzy","zucr","zudr","zulj","zumv","zunt","zuqi","zurf","zusi","zutl","zutp","zuvu","zuwp","zuzl","zvao","zvcd","zvcr","zvdm","zvfh","zvgm","zvhg","zvhr","zvil","zvmj","zvrk","zvtf","zvui","zvxa","zvxb","zvxt","zvxx","zvyf","zvzq","zvzx","zwbr","zwbx","zwel","zwfh","zwhx","zwin","zwjh","zwkm","zwmn","zwoc","zwpq","zwtj","zwwk","zwxb","zxhq","zxiw","zxkr","zxpi","zxtj","zxzi","zybb","zybf","zydg","zyem","zyep","zyjp","zykh","zylv","zyoh","zyph","zyqf","zyuy","zzah","zzcv","zzfe","zzft","zzgw","zzkr","zzmc","zzmh","zzoh","zzqz","zzrz","zzxr","zzzw"]; export default roomNames; ================================================ FILE: backend/src/rooms/room-sagas.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { delay, takeEvery, takeLatest } from 'redux-saga'; import { call, cancel, cancelled, put, fork, spawn, select } from 'redux-saga/effects'; import uuid from 'uuid'; import { logger } from '../logger'; import { messageHandler } from '../messages'; import { serverActions, serverConstants } from '../server'; import { sphereConstants } from '../spheres'; import { makeWsReplyMessage, makeWsBroadcastMessage, sendWsMessageWithLogger, sendWsErrorWithLogger } from '../utils/websocket-utils'; import config from '../config'; import { sagaUtils } from '../utils'; import messageConstants from '../messages/message-constants'; import roomDataActions from './room-data-actions'; import roomDataConstants from './room-data-constants'; import roomNames from './room-names'; import roomStateActions from './room-state-actions'; import roomStateConstants from './room-state-constants'; import roomStateUtils from './room-state-utils'; const incomingMsgComponents = messageConstants.INCOMING_MESSAGE_COMPONENTS; const outgoingMsgComponents = messageConstants.OUTGOING_MESSAGE_COMPONENTS; /******************************************************************************* * START SETTING UP THE ROOM *******************************************************************************/ const startRoomSetup = function* ( action ) { let correctState = yield roomStateUtils.roomIsCurrentlyInState( action.roomName, roomStateConstants.STATES.STARTING_ROOM_SETUP ); if( correctState !== true ) { logger.warn( `not doing startRoomSetup, room ${action.roomName} not in state STARTING_ROOM_SETUP` ); return; } // tell the state machine this worked OK yield put( roomStateActions.notifyStartRoomSetupSuccessRequestAction( action.roomName ) ); }; const handleStartRoomSetupSuccess = function* ( action ) { let correctState = yield roomStateUtils.roomIsCurrentlyInState( action.roomName, roomStateConstants.STATES.ROOM_SETUP_STARTED ); if( correctState !== true ) { logger.warn( `not doing handleStartRoomSetupSuccess, room ${action.roomName} not in state ROOM_SETUP_STARTED` ); return; } // initialise the room state yield put( roomStateActions.initRoomContentRequestAction( action.roomName ) ); // check if it was initialised correctly let roomData = yield select( ( state ) => { return state.roomDataReducer.rooms[ action.roomName ]; } ); // tell the state machine what happened if( typeof roomData.content === 'object' ) { yield put( roomStateActions.notifyInitRoomContentSuccessRequestAction( action.roomName ) ); } else { yield put( roomStateActions.notifyInitRoomContentFailureRequestAction( action.roomName ) ); } }; /******************************************************************************* * HANDLE SUCCESSFUL ROOM STATE INIT *******************************************************************************/ const handleInitRoomContentSuccess = function* ( action ) { let correctState = yield roomStateUtils.roomIsCurrentlyInState( action.roomName, roomStateConstants.STATES.ROOM_CONTENT_INITED ); if( correctState !== true ) { logger.warn( `not doing handleInitRoomContentSuccess, room ${action.roomName} not in state ROOM_CONTENT_INITED` ); return; } // this is possibly an unnecessary state change(!), but it's explicit yield put( roomStateActions.startRoomHeartbeatRequestAction( action.roomName ) ); }; /******************************************************************************* * START HEARTBEAT FOR ROOM *******************************************************************************/ const startHeartbeatForRoom = function* ( action ) { let correctState = yield roomStateUtils.roomIsCurrentlyInState( action.roomName, roomStateConstants.STATES.STARTING_HEARTBEAT ); if( correctState !== true ) { logger.warn( `not doing startHeartbeatForRoom, room ${action.roomName} not in state ${acceptableStates}` ); return; } let { roomData, roomState } = yield select( ( state ) => { return { roomData: state.roomDataReducer.rooms, roomState: state.roomStateReducer }; } ); let lastHeartbeat, heartbeatCount; try { lastHeartbeat = roomData[ action.roomName ].content.lastHeartbeat; let roomStatus = roomState.rooms[ action.roomName ].status; logger.trace( `starting heartbeat for new room: ${action.roomName}` ); } catch( error ) { logger.error( `error trying to start heartbeat for room '${action.roomName}': ${error.message}` ); yield put( roomStateActions.notifyStartRoomHeartbeatFailureRequestAction( action.roomName ) ); return; } // fork a heartbeat task let heartbeatTask = yield fork( heartbeatTaskFunction, action.roomName ); // set it in state yield put( roomDataActions.setHeartbeatTaskForRoomRequestAction( heartbeatTask, action.roomName ) ); // check it got there roomData = yield select( ( state ) => { return state.roomDataReducer.rooms; } ); // tell the state-machine for the room what's happening if( roomData[ action.roomName ].tasks.heartbeat === heartbeatTask ) { yield put( roomStateActions.notifyStartRoomHeartbeatSuccessRequestAction( action.roomName ) ); } else { yield put( roomStateActions.notifyStartRoomHeartbeatFailureRequestAction( action.roomName ) ); } return; }; const heartbeatTaskFunction = function* ( roomName ) { try { let lastHeartbeatCount = 0; while( true ) { // get the latest status let roomData = yield select( ( state ) => { return state.roomDataReducer.rooms; } ); // if the state's been cleared, stop sending if( !roomData[ roomName ] ) { logger.trace( `state for room ${roomName} has been cleared, finishing heartbeat` ); break; } // update the second count let secondCount = ( lastHeartbeatCount * config.roomHeartbeatDelayInMs ) / 1000 ; // create a heartbeat message let heartbeatData = { [ outgoingMsgComponents.ROOM_HEARTBEAT.COUNT ]: lastHeartbeatCount, [ outgoingMsgComponents.ROOM_HEARTBEAT.SECONDS ]: secondCount }; let heartbeatMessage = makeWsBroadcastMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.ROOM_HEARTBEAT, heartbeatData ); // set the latest count in the state yield put( roomDataActions.setLastHeartbeatCountForRoomRequestAction( lastHeartbeatCount, roomName ) ); // publish to room yield put( roomDataActions.publishMessageToRoomRequestAction( heartbeatMessage, roomName ) ); // update the heartbeat count lastHeartbeatCount = lastHeartbeatCount + 1; // pause until next heartbeat yield delay( config.roomHeartbeatDelayInMs ); } } finally { if( yield cancelled() ) { logger.trace( `cancelling heartbeat task for room ${roomName} on request` ); yield put( roomDataActions.removeHeartbeatTaskForRoomRequestAction( roomName ) ); yield put( roomStateActions.notifyStopHeartbeatSuccessRequestAction( roomName ) ); } } }; /******************************************************************************* * ADD QUEUED WEBSOCKETS INTO ROOM AFTER SETUP *******************************************************************************/ const startProcessingQueue = function* ( action ) { let correctState = yield roomStateUtils.roomIsCurrentlyInState( action.roomName, roomStateConstants.STATES.HEARTBEAT_STARTED ); if( correctState !== true ) { logger.warn( `not doing startProcessingQueue, room ${action.roomName} not in state ${acceptableStates}` ); return; } // tell the state machine the queue's being processed yield put( roomStateActions.startProcessingQueueRequestAction( action.roomName ) ); let { serverState, roomData } = yield select( ( state ) => { return { serverState: state.serverReducer, roomData: state.roomDataReducer.rooms }; } ); logger.trace( `processing ${roomData[ action.roomName ].queue.length} websockets in queue for room ${action.roomName}` ); // if all the local clients in the queue left before the setup completed if( roomData[ action.roomName ].queue.length < 1 ) { logger.trace( `no clients left in queue for room ${action.roomName}, closing down room` ); // start closing down the room on this server yield put( roomStateActions.checkIfRoomEmptyRequestAction( action.roomName ) ); return; } for( let clientId of roomData[ action.roomName ].queue ) { let clientHeadsetType = serverState.websockets[ clientId ].headsetType; yield put( roomDataActions.addLocalClientToRoomRequestAction( clientId, clientHeadsetType, action.roomName ) ); yield put( roomDataActions.removeWebsocketFromQueueForRoomRequestAction( clientId, action.roomName ) ); } // tell the state machine the queue's been processed yield put( roomStateActions.notifyProcessQueueSuccessRequestAction( action.roomName ) ); }; const handleProcessQueueSuccess = function* ( action ) { let correctState = yield roomStateUtils.roomIsCurrentlyInState( action.roomName, roomStateConstants.STATES.READY ); if( correctState !== true ) { logger.warn( `not doing handleProcessQueueSuccess, room ${action.roomName} not in state READY` ); return; } let roomData = yield select( ( state ) => { return state.roomDataReducer.rooms; } ); // if the room's full if( Object.keys( roomData[ action.roomName ].content.clients ).length >= config.maxClientsPerRoom ) { // tell the state machine about it yield put( roomStateActions.setRoomFullRequestAction( action.roomName ) ); } }; /******************************************************************************* * CONNECT A WEBSOCKET CONNECTION TO A SPECIFIED ROOM *******************************************************************************/ const connectWebsocketToRoom = function* ( action ) { // belt & braces if( roomNames.indexOf( action.ws.requestedRoomName ) < 0 ) { let noSuchRoomMessage = makeWsReplyMessage( config.serverId, messageConstants.ERROR_TYPES.NO_SUCH_ROOM, { roomName } ); sendWsMessageWithLogger( action.ws, noSuchRoomMessage, logger ); return; } logger.trace( `joining ${action.ws.headsetType} websocket ${action.ws.id} ` + `to room '${action.ws.requestedRoomName}'` ); let roomName = action.ws.requestedRoomName; // get the current state for chosen room let { roomData, roomState } = yield select( ( state ) => { return { roomData: state.roomDataReducer.rooms[ roomName ], roomState: state.roomStateReducer.rooms[ roomName ] }; } ); // work out what FSM state it's in switch( roomState.status ) { // idle, waiting for setup case roomStateConstants.STATES.INIT: // kick off the room setup yield put( roomStateActions.initRoomContentRequestAction( roomName ) ); // check if it was initialised correctly let roomData = yield select( ( state ) => { return state.roomDataReducer.rooms[ roomName ]; } ); // tell the state machine what happened if( typeof roomData.content === 'object' ) { yield put( roomStateActions.notifyInitRoomContentSuccessRequestAction( roomName ) ); } else { yield put( roomStateActions.notifyInitRoomContentFailureRequestAction( roomName ) ); return; } // put the connection in the queue for the room yield put( roomDataActions.addWebsocketToQueueForRoomRequestAction( action.ws.id, roomName ) ); break; // in process of being set up case roomStateConstants.STATES.INITING_ROOM_CONTENT: case roomStateConstants.STATES.ROOM_CONTENT_INITED: case roomStateConstants.STATES.STARTING_HEARTBEAT: case roomStateConstants.STATES.HEARTBEAT_STARTED: // check if there's space in the queue if( roomData.queue.length >= config.maxClientsPerRoom ) { let queueFullMessage = makeWsReplyMessage( config.serverId, messageConstants.ERROR_TYPES.ROOM_QUEUE_FULL, { roomName } ); sendWsMessageWithLogger( action.ws, queueFullMessage, logger ); logger.warn( `closing client ${action.ws.id}: queue for ${roomName} is full` ); action.ws.close(); return; } // put the connection in the queue for the room yield put( roomDataActions.addWebsocketToQueueForRoomRequestAction( action.ws.id, roomName ) ); break; // full case roomStateConstants.STATES.ROOM_FULL: let roomFullMessage = makeWsReplyMessage( config.serverId, messageConstants.ERROR_TYPES.BUSY_TRY_AGAIN, {} ); sendWsMessageWithLogger( action.ws, roomFullMessage, logger ); logger.warn( `closing client ${action.ws.id}: queue for ${roomName} is full` ); action.ws.close(); return; // up and running case roomStateConstants.STATES.READY: // add the connection to the room yield put( roomDataActions.addLocalClientToRoomRequestAction( action.ws.id, action.ws.headsetType, roomName ) ); break; // anything else default: logger.warn( `tried to join ${action.ws.id} to room ${roomName} while in state ${roomState.status}` ); let roomUnavailableMessage = makeWsReplyMessage( config.serverId, messageConstants.ERROR_TYPES.ROOM_UNAVAILABLE, { roomName } ); sendWsMessageWithLogger( action.ws, roomUnavailableMessage, logger ); break; } }; /******************************************************************************* * ADD CLIENT TO ROOM DATA AND INFORM EXISTING CLIENTS IT'S JOINED *******************************************************************************/ // action: { clientId, roomName } const handleAddLocalClientToRoom = function* ( action ) { let { serverState, roomState, roomData } = yield select( ( state ) => { return { serverState: state.serverReducer, roomState: state.roomStateReducer, roomData: state.roomDataReducer.rooms }; } ); if( roomState.rooms[ action.roomName ].status === roomStateConstants.STATES.READY ) { // if the room's full if( Object.keys( roomData[ action.roomName ].content.clients ).length >= config.maxClientsPerRoom ) { // tell the state machine about it yield put( roomStateActions.setRoomFullRequestAction( action.roomName ) ); } } let ws = serverState.websockets[ action.clientId ]; if( typeof ws === 'undefined' ) { logger.warn( `error trying to sendRoomInfoToClient: no websocket object for ws id ${action.clientId}` ); return; } let roomName = action.roomName; if( typeof roomName === 'undefined' ) { logger.warn( `error trying to sendRoomInfoToClient: no currentRoom for ws id ${ws.id}` ); sendWsErrorWithLogger( config.serverId, ws, messageConstants.ERROR_TYPES.SYSTEM_ERROR, roomName ); return; } let roomClients = roomData[ roomName ].content.clients; let clients = {}; // copy of client list, without the connecting client Object.keys( roomClients ).filter( ( clientId ) => { return clientId !== action.clientId; } ) .forEach( ( clientId ) => { const headsetType = roomClients[ clientId ].headsetType; if( typeof clients[ headsetType ] === 'undefined' ) { clients[ headsetType ] = [] } clients[ headsetType ].push( clientId ); } ); // get copy of spheres from room state let spheres = JSON.parse( JSON.stringify( roomData[ roomName ].content.spheres ) ); // add to client message let roomInfo = { [ outgoingMsgComponents.ROOM_STATUS_INFO.ROOM_NAME ]: roomName, [ outgoingMsgComponents.ROOM_STATUS_INFO.SOUNDBANK ]: roomData[ roomName ].content.soundbank, [ outgoingMsgComponents.ROOM_STATUS_INFO.CLIENTS ]: clients, [ outgoingMsgComponents.ROOM_STATUS_INFO.SPHERES ]: spheres }; let roomInfoMessage = makeWsReplyMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.ROOM_STATUS_INFO, roomInfo ); try { sendWsMessageWithLogger( ws, roomInfoMessage, logger ); } catch( error ) { logger.trace( `error sending room info message to client ${ws.id}`, error.message ); } // tell all other clients in the room that this one has joined let clientData = { [ outgoingMsgComponents.ROOM_CLIENT_JOIN.CLIENT_ID ]: ws.id, [ outgoingMsgComponents.ROOM_CLIENT_JOIN.CLIENT_HEADSET_TYPE ]: ws.headsetType }; let clientJoinMessage = makeWsBroadcastMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.ROOM_CLIENT_JOIN, clientData ); yield put( roomDataActions.publishMessageToRoomRequestAction( clientJoinMessage, roomName ) ); // start off a client inactivty check, to eject the client when it goes idle yield fork( clientInactivityTimeoutTaskFunction, ws.id, ws.headsetType ); }; const clientInactivityTimeoutTaskFunction = function* ( clientId, headsetType ) { let serverState; let ws; let inactivityTimeoutPeriod = roomDataConstants.CLIENT_INFO.CLIENT_INACTIVITY_TIMEOUTS_IN_MS[ headsetType ]; try { do { serverState = yield select( ( state ) => { return state.serverReducer; } ); ws = serverState.websockets[ clientId ]; // websocket has disconnected, finish if( typeof ws === 'undefined' ) { break; } // websocket has gone too long without activity, eject if( Date.now() - ws.lastAction > inactivityTimeoutPeriod ) { // tell the client why it's being closed let clientInactivityTimeoutMsg = makeWsReplyMessage( config.serverId, messageConstants.ERROR_TYPES.CLIENT_INACTIVITY_TIMEOUT, undefined ); sendWsMessageWithLogger( ws, clientInactivityTimeoutMsg, logger ); // close it ws.close(); // stop checking this client break; } yield delay( roomDataConstants.CLIENT_INFO.CLIENT_INACTIVITY_TIMEOUT_CHECKS_IN_MS[ headsetType ] ); } while( true ); } catch( error ) { logger.trace( `error checking client activity: ${error.message}` ); } }; /******************************************************************************* * REMOVE CLIENT FROM ROOM DATA AND INFORM EXISTING CLIENTS IT'S LEFT *******************************************************************************/ // when a local client is removed from a room, check whether it's got any local clients left // action: { clientId, roomName } const handleRemoveLocalClientFromRoom = function* ( action ) { let { roomState, roomData } = yield select( ( state ) => { return { roomState: state.roomStateReducer, roomData: state.roomDataReducer.rooms }; } ); // find any spheres (THERE CAN BE ONLY ONE) held by this client ... let spheresHeld = Object.keys( roomData[ action.roomName ].content.spheres ).filter( ( sphereId ) => { if( !roomData[ action.roomName ].content.spheres[ sphereId ].hold ) { return false; } return roomData[ action.roomName ].content.spheres[ sphereId ].hold.clientId === action.clientId; } ); // ... and release each one (THERE CAN BE ONLY ONE) for( let sphereId of spheresHeld ) { yield call( cancelSphereHold, sphereId, action.clientId, action.roomName ); } if( roomState.rooms[ action.roomName ].status === roomStateConstants.STATES.ROOM_FULL ) { // if the room's full if( Object.keys( roomData[ action.roomName ].content.clients ).length < config.maxClientsPerRoom ) { // tell the state machine about it yield put( roomStateActions.unsetRoomFullRequestAction( action.roomName ) ); } } // tell remaining clients that this one's left try { let clientExitMessage = makeWsBroadcastMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.ROOM_CLIENT_EXIT, { [ outgoingMsgComponents.ROOM_CLIENT_EXIT.CLIENT_ID ]: action.clientId } ); yield put( roomDataActions.publishMessageToRoomRequestAction( clientExitMessage, action.roomName ) ); } catch( error ) { logger.trace( `broadcast error trying to notify client exit for ${action.clientId}: ${error.message}` ); } // tell the state machine to move to CHECKING_IF_EMPTY yield put( roomStateActions.checkIfRoomEmptyRequestAction( action.roomName ) ); }; /******************************************************************************* * REMOVE A SPHERE HOLD WHEN CLIENT LEAVES OR TIMES OUT *******************************************************************************/ const cancelSphereHold = function* ( sphereId, clientId, roomName) { // remove the hold on this sphere for this client yield put( roomDataActions.removeHoldOnSphereForClientInRoomRequestAction( sphereId, clientId, roomName ) ); // broadcast to the room that the sphere's been released let outgoingMsgData = { [ outgoingMsgComponents.ROOM_SPHERE_RELEASED.SPHERE_ID ]: sphereId, [ outgoingMsgComponents.ROOM_SPHERE_RELEASED.CLIENT_ID ]: clientId }; let sphereReleasedMessage = makeWsBroadcastMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.ROOM_SPHERE_RELEASED, outgoingMsgData ); yield put( roomDataActions.publishMessageToRoomRequestAction( sphereReleasedMessage, roomName ) ); }; /******************************************************************************* * WORK OUT WHETHER ANY CLIENTS ARE LEFT IN A ROOM, CLOSE ROOM IF NOT *******************************************************************************/ // asked explicitly by state-machine event CHECK_IF_EMPTY const checkIfRoomEmpty = function* ( action ) { let correctState = yield roomStateUtils.roomIsCurrentlyInState( action.roomName, roomStateConstants.STATES.CHECKING_IF_EMPTY ); if( correctState !== true ) { logger.warn( `not doing checkIfRoomEmpty, room ${action.roomName} not in state CHECKING_IF_EMPTY` ); return; } let roomData = yield select( ( state ) => { return state.roomDataReducer.rooms; } ); let numberClientsLeft = Object.keys( roomData[ action.roomName ].content.clients ).length; if( numberClientsLeft > 0 ) { yield put( roomStateActions.notifyRoomEmptyCheckFailureRequestAction( action.roomName ) ); } else { yield put( roomStateActions.notifyRoomEmptyCheckSuccessRequestAction( action.roomName ) ); } }; // if a room is empty of local clients const handleRoomEmptyCheckSuccess = function* ( action ) { let correctState = yield roomStateUtils.roomIsCurrentlyInState( action.roomName, roomStateConstants.STATES.ROOM_EMPTY ); if( correctState !== true ) { logger.warn( `not doing handleRoomEmptyCheckSuccess, room ${action.roomName} not in state ROOM_EMPTY` ); return; } let roomData = yield select( ( state ) => { return state.roomDataReducer.rooms; }); // if there's a heartbeat task running, it should stop if( typeof roomData[ action.roomName ].tasks.heartbeat !== undefined ) { yield put( roomStateActions.stopHeartbeatForRoomRequestAction( action.roomName ) ); return; } }; // asked explicitly by state-machine event STOP_HEARTBEAT const stopHeartbeatForRoom = function* ( action ) { let correctState = yield roomStateUtils.roomIsCurrentlyInState( action.roomName, roomStateConstants.STATES.STOPPING_HEARTBEAT ); if( correctState !== true ) { logger.warn( `not doing stopHeartbeatForRoom, room ${action.roomName} not in state STOPPING_HEARTBEAT` ); return; } let roomData = yield select( ( state ) => { return state.roomDataReducer.rooms; } ); // belt & braces double-check to avoid crashing on cancel if( typeof roomData[ action.roomName ].tasks.heartbeat !== undefined ) { // ask the heartbeatTaskFunction to cancel yield cancel( roomData[ action.roomName ].tasks.heartbeat ); } else { logger.warn( `problem stopping heartbeat for room ${action.roomName}: no task found` ); yield put( roomStateActions.notifyStopHeartbeatFailureRequestAction( action.roomName ) ); } }; // once a server's stopped sending heartbeats for a room, it should re-INIT the room const handleStopHeartbeatSuccessEvent = function* ( action ) { let correctState = yield roomStateUtils.roomIsCurrentlyInState( action.roomName, roomStateConstants.STATES.HEARTBEAT_STOPPED ); if( correctState !== true ) { logger.warn( `not doing handleStopHeartbeatSuccessEvent, room ${action.roomName} not in state HEARTBEAT_STOPPED` ); return; } yield put( roomStateActions.closeRoomRequestAction( action.roomName ) ); }; // just log out room close event const handleRoomCloseEvent = function* ( action ) { let correctState = yield roomStateUtils.roomIsCurrentlyInState( action.roomName, roomStateConstants.STATES.INIT ); if( correctState !== true ) { logger.warn( `not doing handleRoomCloseEvent, room ${action.roomName} not in state INIT` ); return; } logger.trace( `closed room ${action.roomName}` ); }; /******************************************************************************* * START A HOLD TIMEOUT WHEN A CLIENT GRABS A SPHERE *******************************************************************************/ // triggers on roomDataActions.createHoldOnSphereForClientInRoomRequestAction // action: { sphereId, clientId, roomName } const startSphereHoldTimeout = function* ( action ) { if( config.timeoutSphereHolds !== true ) { return; } let sphereHoldTimeoutTask = yield fork( sphereHoldTimeoutTaskFunction, action.sphereId, action.roomName ); yield put( roomDataActions.setHoldTimeoutTaskForSphereInRoomRequestAction( sphereHoldTimeoutTask, action.sphereId, action.roomName ) ); }; const sphereHoldTimeoutTaskFunction = function* ( sphereId, roomName ) { try { let sphereState; let lastSphereAction; do { sphereState = yield select( ( state ) => { return state.roomDataReducer.rooms[ roomName ].content.spheres; } ); if( typeof sphereState[ sphereId ] === 'undefined' ) { // sphere has been deleted break; } if( typeof sphereState[ sphereId ].hold === 'undefined' ) { // sphere has been released break; } // check the last time anything was done with the sphere lastSphereAction = sphereState[ sphereId ].hold.ts; if( Date.now() - lastSphereAction >= roomDataConstants.SPHERE_INFO.SPHERE_HOLD_TIMEOUT_IN_MS ) { // work out which client's holding it let clientId = sphereState[ sphereId ].hold.clientId; // get the websocket for the client let serverState = yield select( ( state ) => { return state.serverReducer; } ); let client = serverState.websockets[ clientId ]; let timeoutMsgData = { [ messageConstants.OUTGOING_MESSAGE_COMPONENTS.SPHERE_HOLD_TIMEOUT.SPHERE_ID ]: sphereId }; // tell the client it's losing the hold let sphereHoldTimeOutMessage = makeWsReplyMessage( config.serverId, messageConstants.ERROR_TYPES.SPHERE_HOLD_TIMEOUT, timeoutMsgData ); sendWsMessageWithLogger( client, sphereHoldTimeOutMessage, logger ); // delete the hold, tell the room yield call( cancelSphereHold, sphereId, clientId, roomName ); break; } yield delay( roomDataConstants.SPHERE_INFO.SPHERE_HOLD_TIMEOUT_CHECK_IN_MS ); } while ( true ); } catch( error ) { logger.warn( `error in sphere hold timeout task for sphere ${sphereId}: ${error.message}` ); } finally { yield put( roomDataActions.deleteHoldTimeoutTaskForSphereInRoomRequestAction( sphereId, roomName ) ); } }; /******************************************************************************* * PUBLISH A MESSAGE TO CLIENTS IN A ROOM, EXCEPT THE SENDER *******************************************************************************/ const publishMessageToRoom = function* ( action ) { let roomName = action.roomName; let messagePacket = action.message; let messageFromId = messagePacket[ outgoingMsgComponents.ALL_MESSAGES.FROM ]; let messageDataEnvelope = messagePacket[ outgoingMsgComponents.ALL_MESSAGES.MSG ]; let messageData = messageDataEnvelope[ outgoingMsgComponents.ALL_MESSAGES.DATA ]; // construct a message for interested websockets let websocketMessage = { [ outgoingMsgComponents.ALL_MESSAGES.FROM ]: messageFromId, [ outgoingMsgComponents.ALL_MESSAGES.MSG ]: messageDataEnvelope }; let { roomData, serverState } = yield select( ( state ) => { return { roomData: state.roomDataReducer.rooms, serverState: state.serverReducer } } ); let roomClients = roomData[ roomName ].content.clients; let roomClientIds = Object.keys( roomClients ); let clientsLength = roomClientIds.length; let serverSockets = serverState.websockets; for( let i = 0; i < clientsLength; i++ ) { let clientId = roomClientIds[ i ]; if( clientId === messageFromId ) { continue; } if( clientId === messageData[ outgoingMsgComponents.ALL_MESSAGES.CLIENT_ID ] ) { continue; } if( typeof serverSockets[ clientId ] === 'undefined' ) { continue; } let ws = serverSockets[ clientId ]; try { sendWsMessageWithLogger( ws, websocketMessage, logger ); } catch( error ) { logger.warn( `error sending ${messageType} message to client ${clientId}: ${error.message}` ); } } }; /******************************************************************************* * WATCHER FUNCTIONS TO CATCH REDUX ACTIONS *******************************************************************************/ const watchStartRoomSetupRequests = function* () { yield takeEvery( roomStateConstants.EVENT_TYPES.START_ROOM_SETUP, startRoomSetup ); }; const watchStartRoomSetupSuccessEvents = function* () { yield takeEvery( roomStateConstants.EVENT_TYPES.START_ROOM_SETUP_SUCCESS, handleStartRoomSetupSuccess ); }; const watchInitRoomContentSuccessEvents = function* () { yield takeEvery( roomStateConstants.EVENT_TYPES.INIT_ROOM_CONTENT_SUCCESS, handleInitRoomContentSuccess ); }; const watchStartHeartbeatRequestEvents = function* () { yield takeEvery( roomStateConstants.EVENT_TYPES.START_HEARTBEAT, startHeartbeatForRoom ); }; const watchStartHeartbeatSuccessEvents = function* () { yield takeEvery( roomStateConstants.EVENT_TYPES.START_HEARTBEAT_SUCCESS, startProcessingQueue ); }; const watchProcessQueueSuccessEvents = function* () { yield takeEvery( roomStateConstants.EVENT_TYPES.PROCESS_QUEUE_SUCCESS, handleProcessQueueSuccess ); }; const watchAddWebsocketToServerStateRequests = function* () { yield takeEvery( serverConstants.ACTION_TYPES.ADD_WEBSOCKET_TO_STATE, connectWebsocketToRoom ); }; const watchAddLocalClientToRoomRequests = function* () { yield takeEvery( roomDataConstants.ACTION_TYPES.ADD_LOCAL_CLIENT_TO_ROOM, handleAddLocalClientToRoom ); }; const watchPublishMessageRequestActions = function* () { yield takeEvery( roomDataConstants.ACTION_TYPES.PUBLISH_MESSAGE_TO_ROOM, publishMessageToRoom ); }; const watchStopHeartbeatRequests = function* () { yield takeEvery( roomStateConstants.EVENT_TYPES.STOP_HEARTBEAT, stopHeartbeatForRoom ); }; const watchStopHeartbeatSuccessEvents = function* () { yield takeEvery( roomStateConstants.EVENT_TYPES.STOP_HEARTBEAT_SUCCESS, handleStopHeartbeatSuccessEvent ); }; const watchRoomCloseEvents = function* () { yield takeEvery( roomStateConstants.EVENT_TYPES.CLOSE, handleRoomCloseEvent ); }; const watchCreateHoldOnSphereEvents = function* () { yield takeEvery( roomDataConstants.ACTION_TYPES.CREATE_HOLD_ON_SPHERE_FOR_CLIENT_IN_ROOM, startSphereHoldTimeout ); }; const watchRemoveLocalClientFromRoomRequests = function* () { yield takeEvery( roomDataConstants.ACTION_TYPES.REMOVE_LOCAL_CLIENT_FROM_ROOM, handleRemoveLocalClientFromRoom ); }; const watchCheckIfRoomEmptyRequests = function* () { yield takeEvery( roomStateConstants.EVENT_TYPES.CHECK_IF_EMPTY, checkIfRoomEmpty ); }; const watchRoomEmptyCheckSuccessEvents = function* () { yield takeEvery( roomStateConstants.EVENT_TYPES.EMPTY_CHECK_SUCCESS, handleRoomEmptyCheckSuccess ); }; /******************************************************************************* * EXPORT SAGAS *******************************************************************************/ export default { setupSaga: function* () { const sagas = [ watchStartRoomSetupRequests, watchStartRoomSetupSuccessEvents, watchInitRoomContentSuccessEvents, watchStartHeartbeatRequestEvents, watchStartHeartbeatSuccessEvents, watchProcessQueueSuccessEvents, ]; yield call( sagaUtils.spawnAutoRestartingSagas, sagas ); }, connectionSaga: function* () { const sagas = [ watchAddWebsocketToServerStateRequests, watchAddLocalClientToRoomRequests, ]; yield call( sagaUtils.spawnAutoRestartingSagas, sagas ); }, publishSaga: function* () { const sagas = [ watchPublishMessageRequestActions ]; yield call( sagaUtils.spawnAutoRestartingSagas, sagas ); }, heartbeatSaga: function* () { const sagas = [ watchStopHeartbeatRequests, watchStopHeartbeatSuccessEvents, watchRoomCloseEvents ]; yield call( sagaUtils.spawnAutoRestartingSagas, sagas ); }, sphereHoldTimeoutSaga: function* () { const sagas = [ watchCreateHoldOnSphereEvents ]; yield call( sagaUtils.spawnAutoRestartingSagas, sagas ); }, disconnectionSaga: function* () { const sagas = [ watchRemoveLocalClientFromRoomRequests, watchCheckIfRoomEmptyRequests, watchRoomEmptyCheckSuccessEvents ]; yield call( sagaUtils.spawnAutoRestartingSagas, sagas ); } }; ================================================ FILE: backend/src/rooms/room-state-actions.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import roomStateConstants from './room-state-constants'; // setup room const startRoomSetupRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.START_ROOM_SETUP; return { type, roomName }; }; const notifyStartRoomSetupSuccessRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.START_ROOM_SETUP_SUCCESS; return { type, roomName }; }; const notifyStartRoomSetupFailureRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.START_ROOM_SETUP_FAILURE; return { type, roomName }; }; // initialise room content const initRoomContentRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.INIT_ROOM_CONTENT; return { type, roomName }; }; const notifyInitRoomContentSuccessRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.INIT_ROOM_CONTENT_SUCCESS; return { type, roomName }; }; const notifyInitRoomContentFailureRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.INIT_ROOM_CONTENT_FAILURE; return { type, roomName }; }; // start a heartbeat for a room const startRoomHeartbeatRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.START_HEARTBEAT; return { type, roomName }; }; const notifyStartRoomHeartbeatSuccessRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.START_HEARTBEAT_SUCCESS; return { type, roomName }; }; const notifyStartRoomHeartbeatFailureRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.START_HEARTBEAT_FAILURE; return { type, roomName }; }; // processing the queue for a newly-inited room const startProcessingQueueRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.PROCESS_QUEUE; return { type, roomName }; }; const notifyProcessQueueSuccessRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.PROCESS_QUEUE_SUCCESS; return { type, roomName }; }; const notifyProcessQueueFailureRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.PROCESS_QUEUE_FAILURE; return { type, roomName }; }; // mark a room full or not-full const setRoomFullRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.SET_ROOM_FULL; return { type, roomName }; }; const unsetRoomFullRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.UNSET_ROOM_FULL; return { type, roomName }; }; // check if a room's empty when a client leaves const checkIfRoomEmptyRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.CHECK_IF_EMPTY; return { type, roomName }; }; const notifyRoomEmptyCheckSuccessRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.EMPTY_CHECK_SUCCESS; return { type, roomName }; }; const notifyRoomEmptyCheckFailureRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.EMPTY_CHECK_FAILURE; return { type, roomName }; }; // stop a heartbeat for a room const stopHeartbeatForRoomRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.STOP_HEARTBEAT; return { type, roomName }; }; const notifyStopHeartbeatSuccessRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.STOP_HEARTBEAT_SUCCESS; return { type, roomName }; }; const notifyStopHeartbeatFailureRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.STOP_HEARTBEAT_FAILURE; return { type, roomName }; }; // close a room so it can be re-opened const closeRoomRequestAction = ( roomName ) => { let type = roomStateConstants.EVENT_TYPES.CLOSE; return { type, roomName }; }; export default { // setup a room on request startRoomSetupRequestAction, notifyStartRoomSetupSuccessRequestAction, notifyStartRoomSetupFailureRequestAction, // initialise room content initRoomContentRequestAction, notifyInitRoomContentSuccessRequestAction, notifyInitRoomContentFailureRequestAction, // start a heartbeat for a room startRoomHeartbeatRequestAction, notifyStartRoomHeartbeatSuccessRequestAction, notifyStartRoomHeartbeatFailureRequestAction, // processing the queue for a newly-inited room startProcessingQueueRequestAction, notifyProcessQueueSuccessRequestAction, notifyProcessQueueFailureRequestAction, // mark a room full or not-full setRoomFullRequestAction, unsetRoomFullRequestAction, // check if a room's empty when a client leaves checkIfRoomEmptyRequestAction, notifyRoomEmptyCheckSuccessRequestAction, notifyRoomEmptyCheckFailureRequestAction, // stop a heartbeat for a room stopHeartbeatForRoomRequestAction, notifyStopHeartbeatSuccessRequestAction, notifyStopHeartbeatFailureRequestAction, // close a room so it can be re-opened closeRoomRequestAction }; ================================================ FILE: backend/src/rooms/room-state-constants.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const MODULE_NAME = 'room-state'; const EVENT_TYPES = ( () => { let eventTypes = { START_ROOM_SETUP: 'start_room_setup', START_ROOM_SETUP_SUCCESS: 'start_room_setup_success', START_ROOM_SETUP_FAILURE: 'start_room_setup_failure', INIT_ROOM_CONTENT: 'init_room_content', INIT_ROOM_CONTENT_SUCCESS: 'init_room_content_success', INIT_ROOM_CONTENT_FAILURE: 'init_room_content_failure', START_HEARTBEAT: 'start_heartbeat', START_HEARTBEAT_SUCCESS: 'start_heartbeat_success', START_HEARTBEAT_FAILURE: 'start_heartbeat_failure', PROCESS_QUEUE: 'process_queue', PROCESS_QUEUE_SUCCESS: 'process_queue_success', PROCESS_QUEUE_FAILURE: 'process_queue_failure', SET_ROOM_FULL: 'set_room_full', UNSET_ROOM_FULL: 'unset_room_full', CHECK_IF_EMPTY: 'check_if_empty', EMPTY_CHECK_SUCCESS: 'empty_check_success', EMPTY_CHECK_FAILURE: 'empty_check_failure', STOP_HEARTBEAT: 'stop_heartbeat', STOP_HEARTBEAT_SUCCESS: 'stop_heartbeat_success', STOP_HEARTBEAT_FAILURE: 'stop_heartbeat_failure', CLOSE: 'close', RESET: 'reset', RESET_DONE: 'reset_done' }; Object.keys( eventTypes ).forEach( ( eventType ) => { eventTypes[ eventType ] = `${MODULE_NAME}/${eventTypes[ eventType ]}`; } ); return eventTypes; })(); const STATES = ( () => { let stateNames = { INIT: 'init', ERROR: 'error', STARTING_ROOM_SETUP: 'starting_room_setup', ROOM_SETUP_STARTED: 'room_setup_started', INITING_ROOM_CONTENT: 'initing_room_content', ROOM_CONTENT_INITED: 'room_content_inited', STARTING_HEARTBEAT: 'starting_heartbeat', HEARTBEAT_STARTED: 'heartbeat_started', STOPPING_HEARTBEAT: 'stopping_heartbeat', HEARTBEAT_STOPPED: 'heartbeat_stopped', PROCESSING_QUEUE: 'processing_queue', ROOM_FULL: 'room_full', READY: 'ready', CHECKING_IF_EMPTY: 'checking_if_empty', ROOM_EMPTY: 'room_empty', RESETTING: 'resetting' }; Object.keys( stateNames ).forEach( ( stateName ) => { stateNames[ stateName ] = `${MODULE_NAME}/${stateNames[ stateName ]}`; } ); return stateNames; })(); export { EVENT_TYPES, STATES }; export default { EVENT_TYPES, STATES }; ================================================ FILE: backend/src/rooms/room-state-machine.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * heavily inspired by, and modified to * accommodate multiple-room states from: * https://github.com/realb0t/redux-state-machine * * basically provides a simple Redux wrapper around * https://github.com/jakesgordon/javascript-state-machine * * dynamically creates a new FSM/state-store per room * when requested in Redux actions, e.g.: * { type: 'ACTION_TYPE', roomName: 'my-room' } */ import StateMachine from 'javascript-state-machine'; import sizeof from 'object-sizeof'; import { fill, zipObject } from 'lodash'; import { logger } from '../logger'; import { initStateObject } from '../utils/reducer-utils'; import roomNames from './room-names'; import roomStateConstants from './room-state-constants'; const initialFsmState = roomStateConstants.STATES.INIT; /** * Return new state object with status param * @param {String} status status name param * @param {Object} { action, error } addition params * @return {Object} new state object */ const buildState = ( status, { action = null, error = null } = {} ) => { return { [ status ]: true, action, error, status }; } /** * Default state object * @type {Object} */ const defaultState = {}; /** * @param {Object} fsmConfig - Config as for javascript-state-machine */ const reducerBuilder = ( fsmConfig ) => { const { events } = fsmConfig; const eventNames = events.map( ( event ) => { return event.name; } ); const eventExists = zipObject( eventNames, fill( Array( eventNames.length ), true, 0, eventNames.length ) ); // list of state machine objects keyed by room name const machines = ( () => { let createFunction = () => { return StateMachine.create( Object.assign( {}, fsmConfig, { initialFsmState } ) ); }; return initStateObject( roomNames, createFunction, 'room state machines', logger ); } )(); // dictionary of // .rooms: machine-state objects for rooms to be keyed by room name // .roomsByState: lists of rooms keyed by current state const states = ( () => { let createFunction = () => { return buildState( initialFsmState ); }; return { // objects containing the state for the machine for each room rooms: initStateObject( roomNames, createFunction, 'room state objects', logger ), // lookup table of which rooms are in which state on this server roomsByState: ( () => { let r = {}; Object.keys( roomStateConstants.STATES ).forEach( ( stateName ) => { r[ roomStateConstants.STATES[ stateName ] ] = {}; } ); roomNames.forEach( ( roomName ) => { r[ roomStateConstants.STATES.INIT ][ roomName ] = true; } ); logger.trace( `initialised lookup table of ${Object.keys( r[ roomStateConstants.STATES.INIT ] ).length} rooms to INIT state` ); return r; } )() }; } )(); // Create reducer function const reducer = ( state = states, action ) => { // ignore events the FSM doesn't handle if (typeof eventExists[ action.type ] === 'undefined') { return state; } if( typeof action.type === 'undefined' ) { logger.warn( `state-machine cannot execute ${action.type} action.type, check room-state-constants` ); return state; } if( typeof action.roomName === 'undefined' ) { return state; } // make sure this is a room we have a state machine for if( typeof machines[ action.roomName ] === 'undefined' ) { logger.warn( `FSM for room ${action.roomName} does not exist` ); return state; } // make sure the states this module keeps track of includes this room if( typeof states.rooms[ action.roomName ] === 'undefined' ) { logger.warn( `FSM state for room ${action.roomName} does not exist` ); return state; } /* logger.warn( `state machine asked to do ${action.type} from ${states.rooms[ action.roomName ].status}` ); */ // keep the two in sync if ( machines[ action.roomName ].current !== states.rooms[ action.roomName ].status ) { machines[ action.roomName ].current = states.rooms[ action.roomName ].status; } // check the requested transition is possible if ( machines[ action.roomName ].cannot( action.type ) ) { logger.error( `room ${action.roomName} cannot do ${action.type} from ${states.rooms[ action.roomName ].status}` ); // set error if not states.rooms[ action.roomName ] = { status: states.rooms[ action.roomName ].status, error: true, [ states.rooms[ action.roomName ].status ]: true, action }; return states; } let previousState = states.rooms[ action.roomName ].status; // fire event as function machines[ action.roomName ][ action.type ]( action ); // return state list containing new state for this room after transition states.rooms[ action.roomName ] = { status: JSON.parse( JSON.stringify( machines[ action.roomName ].current ) ), error: null, [ JSON.parse( JSON.stringify( machines[ action.roomName ].current ) ) ]: true, action }; // states.roomsByState[ STATES.FETCHING_TOPIC ][ 'mrko' ] = true; states.roomsByState[ states.rooms[ action.roomName ].status ][ action.roomName ] = true; states.roomsByState[ previousState ][ action.roomName ] = null; delete states.roomsByState[ previousState ][ action.roomName ]; /* logger.warn( `state machine did ${action.type} on ${action.roomName}` ); logger.warn( `room ${action.roomName} is now in state ${states.rooms[ action.roomName ].status}` ); */ return states; }; // Open FSMs reducer.machines = machines; reducer.states = states; reducer.eventExists = eventExists; return reducer; }; export default reducerBuilder; ================================================ FILE: backend/src/rooms/room-state-reducer.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import reducerBuilder from './room-state-machine'; import { EVENT_TYPES, STATES } from './room-state-constants'; let e = EVENT_TYPES, s = STATES; const reducer = reducerBuilder({ events: [ // send a start message so room-sagas can send a PubSub syncRoomSetup message { name: e.INIT_ROOM_CONTENT, from: s.INIT, to: s.INITING_ROOM_CONTENT }, { name: e.INIT_ROOM_CONTENT_SUCCESS, from: s.INITING_ROOM_CONTENT, to: s.ROOM_CONTENT_INITED }, { name: e.INIT_ROOM_CONTENT_FAILURE, from: s.INITING_ROOM_CONTENT, to: s.ERROR }, // once it's initialised the room, it should start a heartbeat { name: e.START_HEARTBEAT, from: s.ROOM_CONTENT_INITED, to: s.STARTING_HEARTBEAT }, { name: e.START_HEARTBEAT_SUCCESS, from: s.STARTING_HEARTBEAT, to: s.HEARTBEAT_STARTED }, { name: e.START_HEARTBEAT_FAILURE, from: s.STARTING_HEARTBEAT, to: s.ERROR }, // when it's started a heartbeat, the server should connect websockets from the queue { name: e.PROCESS_QUEUE, from: s.HEARTBEAT_STARTED, to: s.PROCESSING_QUEUE }, // when it's received all state from peers, the server should connect websockets from the queue { name: e.PROCESS_QUEUE, from: s.ALL_STATE_RECEIVED, to: s.PROCESSING_QUEUE }, { name: e.PROCESS_QUEUE_SUCCESS, from: s.PROCESSING_QUEUE, to: s.READY }, { name: e.PROCESS_QUEUE_FAILURE, from: s.PROCESSING_QUEUE, to: s.ERROR }, // if all the clients in the queue left before the subscription was completed, start room closedown { name: e.CHECK_IF_EMPTY, from: s.PROCESSING_QUEUE, to: s.CHECKING_IF_EMPTY }, // when config.maxClients have arrived, the server should mark the room as full { name: e.SET_ROOM_FULL, from: s.READY, to: s.ROOM_FULL }, { name: e.UNSET_ROOM_FULL, from: s.ROOM_FULL, to: s.READY }, // when a client leaves the room, the server should check if there are any local clients left { name: e.CHECK_IF_EMPTY, from: s.READY, to: s.CHECKING_IF_EMPTY }, { name: e.CHECK_IF_EMPTY, from: s.ROOM_FULL, to: s.CHECKING_IF_EMPTY }, // if so, it should just go back to waiting for others { name: e.EMPTY_CHECK_FAILURE, from: s.CHECKING_IF_EMPTY, to: s.READY }, // it not, it should unsubscribe from the room { name: e.EMPTY_CHECK_SUCCESS, from: s.CHECKING_IF_EMPTY, to: s.ROOM_EMPTY }, // if there aren't any local clients left, the handleRoomEmpty saga should work out whether to stop a heartbeat { name: e.STOP_HEARTBEAT, from: s.ROOM_EMPTY, to: s.STOPPING_HEARTBEAT }, { name: e.STOP_HEARTBEAT_SUCCESS, from: s.STOPPING_HEARTBEAT, to: s.HEARTBEAT_STOPPED }, { name: e.STOP_HEARTBEAT_FAILURE, from: s.STOPPING_HEARTBEAT, to: s.ERROR }, // when the heartbeat's stopped, reset the room to its INIT state { name: e.CLOSE, from: s.HEARTBEAT_STOPPED, to: s.INIT }, // if it gets into an error state, it should trigger a cleanup action { name: e.RESET, from: s.ERROR, to: s.RESETTING }, // cleaning up a room resets it to init status { name: e.RESET_SUCCESS, from: s.RESETTING, to: s.INIT } ] }); export default reducer; ================================================ FILE: backend/src/rooms/room-state-utils.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { select } from 'redux-saga/effects'; import arrify from 'arrify'; const roomIsCurrentlyInState = function* ( roomName, desiredState ) { let roomState = yield select( ( state ) => { return state.roomStateReducer; } ); if( typeof roomState.rooms[ roomName ] === 'undefined' ) { return false; } let statesToCheck = arrify( desiredState ); let currentState = roomState.rooms[ roomName ].status; let roomIsInDesiredState = false; for( let state of statesToCheck ) { if( state === currentState ) { return true; } } return false; }; export default { roomIsCurrentlyInState }; ================================================ FILE: backend/src/s11n.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const serialize = ( obj ) => { return JSON.stringify( obj ); }; const deserialize = ( msg ) => { try { return JSON.parse( msg ); } catch( error ) { throw error; } }; export { serialize, deserialize }; ================================================ FILE: backend/src/server/app-server.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import express from 'express'; const lbHealthCheckUrl = "/lb-health-check"; /* * here's where we'd implement any custom HTTP routes */ const createApp = () => { let app = express(); // don't announce server software app.use( (req, res, next) => { res.removeHeader('X-Powered-By'); next(); } ); // say something at the root app.get( '/', ( req, res ) => { res.send( `forest\n` ); } ); // lb health check 200 response app.get( lbHealthCheckUrl, ( req, res ) => { res.send( `OK` ); } ); return app; }; export default { createApp }; export { lbHealthCheckUrl }; ================================================ FILE: backend/src/server/balancer.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { logger } from '../logger'; import { messageConstants } from '../messages'; import { roomNames } from '../rooms'; import { dsClient, datastoreConstants } from '../datastore'; import config from '../config'; import { urlUtils } from '../utils'; import roomStateConstants from '../rooms/room-state-constants'; import { getStoreState } from '../store'; import { privatiseTargetServerIfLocal } from './target-server-utils'; import { CUSTOM_PROXY_HEADERS } from './server-constants'; const getBalancerFunction = ( proxy ) => { let lastPortUsed = 0; let headsetTypes = Object.values( messageConstants.HEADSET_TYPES ); let serverReducer = getStoreState().serverReducer; let ring = serverReducer.hashRing; return ( req, socket, head ) => { // make sure client didn't try to use our custom headers Object.values( CUSTOM_PROXY_HEADERS ).forEach( ( headerName ) => { delete req.headers[ headerName ]; } ); let target; let requestInfo; // work out headsetType & optional roomName from URL try { requestInfo = urlUtils.getClientInfoFromUrl( req.url ); // pass headset type in custom proxy request header req.headers[ CUSTOM_PROXY_HEADERS.X_CLIENT_HEADSET_TYPE ] = requestInfo.clientHeadsetType; // pass requested room name if present if( typeof requestInfo.roomName !== 'undefined' ) { req.headers[ CUSTOM_PROXY_HEADERS.X_REQUESTED_ROOM_NAME ] = requestInfo.roomName; } } catch( error ) { // WS client requested an invalid URL // forward to any WS server to close the connection gracefully target = 'ws://' + privatiseTargetServerIfLocal( ring.get( req.url ) ); req.headers[ CUSTOM_PROXY_HEADERS.X_PLEASE_CLOSE_THIS_CONNECTION ] = true; proxy.ws( req, socket, head, { target } ); return; } // a room was specified if( typeof requestInfo.roomName !== 'undefined' ) { // but it wasn't one in the pre-defined list if( roomNames.indexOf( requestInfo.roomName ) < 0 ) { // forward to any WS server to close the connection gracefully target = 'ws://' + privatiseTargetServerIfLocal( ring.get( req.url ) ); req.headers[ CUSTOM_PROXY_HEADERS.X_PLEASE_CLOSE_THIS_CONNECTION ] = true; proxy.ws( req, socket, head, { target } ); return; } // the specified room name was valid, proxy to the appropriate server target = 'ws://' + privatiseTargetServerIfLocal( ring.get( requestInfo.roomName ) ); // tell the WS server this room was specified directly by the client req.headers[ CUSTOM_PROXY_HEADERS.X_CLIENT_SPECIFIED_ROOM_NAME ] = true; proxy.ws( req, socket, head, { target } ); } // no room was specified, choose one else { let chosenRoomName; // try to find one with availability for this type let state = getStoreState(); let avails = state.roomDataReducer.avails[ requestInfo.clientHeadsetType ]; let availableRoomNames = Object.keys( avails ); let availableRoomCount = availableRoomNames.length; if( availableRoomCount > 0 ) { // choose one at random from the available list chosenRoomName = availableRoomNames[ Math.floor( Math.random() * availableRoomCount ) ]; } // otherwise, get an empty room that's servable from this server else { let emptyRoomNames = Object.keys( state.roomStateReducer.roomsByState[ roomStateConstants.STATES.INIT ] ); let servableRoomNames = state.serverReducer.servableRooms; let servableEmptyRoomNames = containsAll( emptyRoomNames, servableRoomNames ); let servableEmptyRoomCount = servableEmptyRoomNames.length; if( servableEmptyRoomCount === 0 ) { // client has to try again return; } chosenRoomName = servableEmptyRoomNames[ Math.floor( Math.random() * servableEmptyRoomCount ) ]; } // the specified room name was valid, proxy to the appropriate server target = 'ws://' + privatiseTargetServerIfLocal( ring.get( chosenRoomName ) ); req.headers[ CUSTOM_PROXY_HEADERS.X_REQUESTED_ROOM_NAME ] = chosenRoomName; proxy.ws( req, socket, head, { target } ); } } }; // http://stackoverflow.com/a/11076088 // ES6 arrow functions don't bind 'arguments' // so this has to be a 'function()' const containsAll = function(/* pass all arrays here */) { var output = []; var cntObj = {}; var array, item, cnt; // for each array passed as an argument to the function for (var i = 0; i < arguments.length; i++) { array = arguments[i]; // for each element in the array for (var j = 0; j < array.length; j++) { item = "-" + array[j]; cnt = cntObj[item] || 0; // if cnt is exactly the number of previous arrays, // then increment by one so we count only one per array if (cnt == i) { cntObj[item] = cnt + 1; } } } // now collect all results that are in all arrays for (item in cntObj) { if (cntObj.hasOwnProperty(item) && cntObj[item] === arguments.length) { output.push(item.substring(1)); } } return(output); }; export { getBalancerFunction }; ================================================ FILE: backend/src/server/index.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import serverActions from './server-actions'; import serverConstants from './server-constants'; import serverReducer from './server-reducer'; import serverSagas from './server-sagas'; export { serverActions, serverConstants, serverReducer, serverSagas }; ================================================ FILE: backend/src/server/server-actions.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import constants from './server-constants'; // reducer actions const setSyncTopicRequestAction = ( syncTopic ) => { let type = constants.ACTION_TYPES.SET_SYNC_TOPIC; return { type, syncTopic }; }; const setSyncSubscriptionRequestAction = ( syncSubscription ) => { let type = constants.ACTION_TYPES.SET_SYNC_SUBSCRIPTION; return { type, syncSubscription }; }; const setAppServerRequestAction = ( appServer ) => { let type = constants.ACTION_TYPES.SET_APP_SERVER; return { type, appServer }; }; const setWebsocketServerRequestAction = ( websocketServer ) => { let type = constants.ACTION_TYPES.SET_WEBSOCKET_SERVER; return { type, websocketServer }; }; const setBalancerRequestAction = ( balancer ) => { let type = constants.ACTION_TYPES.SET_BALANCER; return { type, balancer }; }; const setMonitorTaskForPeerRequestAction = ( monitorTask, peerId ) => { let type = constants.ACTION_TYPES.SET_MONITOR_TASK_FOR_PEER; return { type, monitorTask, peerId }; }; const setLastHeartbeatForPeerRequestAction = ( data, peerId ) => { let type = constants.ACTION_TYPES.SET_LAST_HEARTBEAT_FOR_PEER; return { type, data, peerId }; }; const removePeerFromListRequestAction = ( peerId ) => { let type = constants.ACTION_TYPES.REMOVE_PEER_FROM_LIST; return { type, peerId }; }; const addWebsocketToStateRequestAction = ( ws ) => { let type = constants.ACTION_TYPES.ADD_WEBSOCKET_TO_STATE; return { type, ws }; }; const setWebsocketTimeoutTaskForClientRequestAction = ( timeoutTask, clientId ) => { let type = constants.ACTION_TYPES.SET_WEBSOCKET_TIMEOUT_TASK_FOR_CLIENT; return { type, timeoutTask, clientId }; }; const cancelWebsocketRoomJoinTimeoutTaskForClientRequestAction = ( clientId ) => { let type = constants.ACTION_TYPES.CANCEL_WEBSOCKET_ROOM_JOIN_TIMEOUT_TASK_FOR_CLIENT; return { type, clientId }; }; const removeWebsocketTimeoutTaskForClientRequestAction = ( clientId ) => { let type = constants.ACTION_TYPES.REMOVE_WEBSOCKET_TIMEOUT_TASK_FOR_CLIENT; return { type, clientId }; }; const removeWebsocketFromStateRequestAction = ( ws ) => { let type = constants.ACTION_TYPES.REMOVE_WEBSOCKET_FROM_STATE; return { type, ws }; }; // saga actions const startServerSetupRequestAction = () => { let type = constants.ACTION_TYPES.START_SERVER_SETUP; return { type }; }; const finishServerSetupRequestAction = () => { let type = constants.ACTION_TYPES.FINISH_SERVER_SETUP; return { type }; }; const startSyncSubscriptionWarmupRequestAction = () => { let type = constants.ACTION_TYPES.START_SYNC_SUBSCRIPTION_WARMUP; return { type }; }; const addSyncSubscriptionWarmupResponseRequestAction = () => { let type = constants.ACTION_TYPES.ADD_SYNC_SUBSCRIPTION_WARMUP_RESPONSE; return { type }; }; const startSendingSyncHeartbeatRequestAction = () => { let type = constants.ACTION_TYPES.START_SENDING_SYNC_HEARTBEAT; return { type }; }; const startSavingSubscriptionInfoRequestAction = () => { let type = constants.ACTION_TYPES.START_SAVING_SUBSCRIPTION_INFO; return { type }; }; const loadRateLimiterInfoRequestAction = () => { let type = constants.ACTION_TYPES.LOAD_RATE_LIMITER_INFO_FROM_DATASTORE; return { type }; }; const startCheckingDeadPeerSubscriptionsRequestAction = () => { let type = constants.ACTION_TYPES.START_CHECKING_DEAD_PEER_SUBSCRIPTIONS; return { type }; }; const startMonitorForPeerRequestAction = ( serverId ) => { let type = constants.ACTION_TYPES.START_MONITOR_FOR_PEER; return { type, serverId }; }; export default { // reducer actions setSyncTopicRequestAction, setSyncSubscriptionRequestAction, setAppServerRequestAction, setWebsocketServerRequestAction, setBalancerRequestAction, setMonitorTaskForPeerRequestAction, setLastHeartbeatForPeerRequestAction, removePeerFromListRequestAction, addWebsocketToStateRequestAction, setWebsocketTimeoutTaskForClientRequestAction, cancelWebsocketRoomJoinTimeoutTaskForClientRequestAction, removeWebsocketTimeoutTaskForClientRequestAction, removeWebsocketFromStateRequestAction, // saga actions startServerSetupRequestAction, finishServerSetupRequestAction, startSyncSubscriptionWarmupRequestAction, addSyncSubscriptionWarmupResponseRequestAction, startSendingSyncHeartbeatRequestAction, startSavingSubscriptionInfoRequestAction, loadRateLimiterInfoRequestAction, startCheckingDeadPeerSubscriptionsRequestAction, startMonitorForPeerRequestAction }; ================================================ FILE: backend/src/server/server-constants.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const ACTION_TYPES = { // reducer actions SET_SYNC_TOPIC: 'set_sync_topic', SET_SYNC_SUBSCRIPTION: 'set_sync_subscription', SET_APP_SERVER: 'set_app_server', SET_WEBSOCKET_SERVER: 'set_websocket_server', SET_BALANCER: 'set_balancer', SET_MONITOR_TASK_FOR_PEER: 'set_monitor_task_for_peer', SET_LAST_HEARTBEAT_FOR_PEER: 'set_last_heartbeat_for_peer', REMOVE_PEER_FROM_LIST: 'remove_peer_from_list', ADD_WEBSOCKET_TO_STATE: 'add_websocket_to_state', SET_WEBSOCKET_TIMEOUT_TASK_FOR_CLIENT: 'set_websocket_timeout_task_for_client', CANCEL_WEBSOCKET_ROOM_JOIN_TIMEOUT_TASK_FOR_CLIENT: 'cancel_websocket_room_join_timeout_task_for_client', REMOVE_WEBSOCKET_TIMEOUT_TASK_FOR_CLIENT: 'remove_websocket_timeout_task_for_client', REMOVE_WEBSOCKET_FROM_STATE: 'remove_websocket_from_state', // saga actions START_SERVER_SETUP: 'start_server_setup', FINISH_SERVER_SETUP: 'finish_server_setup', START_SYNC_SUBSCRIPTION_WARMUP: 'start_sync_subscription_warmup', ADD_SYNC_SUBSCRIPTION_WARMUP_RESPONSE: 'add_sync_subscription_warmup_response', START_SENDING_SYNC_HEARTBEAT: 'start_sending_sync_heartbeat', START_SAVING_SUBSCRIPTION_INFO: 'start_saving_subscription_info', LOAD_RATE_LIMITER_INFO_FROM_DATASTORE: 'load_rate_limiter_info_from_datastore', START_CHECKING_DEAD_PEER_SUBSCRIPTIONS: 'start_checking_dead_peer_subscriptions', START_MONITOR_FOR_PEER: 'start_monitor_for_peer', }; const SYNC_INFO = { SYNC_TOPIC_NAME: 'server-sync', SYNC_WARMUP_MESSAGE_LIST_LENGTH: 10, SYNC_HEARTBEAT_PERIOD_IN_MS: 3000, SYNC_HEARTBEAT_TIMEOUT_IN_MS: 15000, }; const SYNC_MESSAGES = { SYNC_HEARTBEAT: 'sync_heartbeat', SYNC_WARMUP: 'sync_warmup', LOAD_RATE_LIMITER_INFO: 'load_rate_limiter_info' }; const saveSubscriptionInfoPeriodInMs = 5000; // save subscription info to datastore every 5s const checkSubscriptionInfoPeriodInMs = saveSubscriptionInfoPeriodInMs * 6; // check for dead peers twice a minute const SAVE_INFO = { SAVE_SUBSCRIPTION_INFO_PERIOD_IN_MS: saveSubscriptionInfoPeriodInMs, CHECK_SUBSCRIPTION_INFO_PERIOD_IN_MS: checkSubscriptionInfoPeriodInMs }; const CUSTOM_PROXY_HEADERS = { X_CLIENT_HEADSET_TYPE: 'x-client-headset-type', X_CLIENT_SPECIFIED_ROOM_NAME: 'x-client-specified-room-name', X_NO_ROOMS_AVAILABLE: 'x-no-rooms-available', X_ROOM_ABOVE_THRESHOLD: 'x-room-above-threshold', X_PLEASE_CLOSE_THIS_CONNECTION: 'x-please-close-this-connection', X_REQUESTED_ROOM_NAME: 'x-requested-room-name' }; const ERROR_TYPES = { ROOM_NAME_REQUIRED: 'room_name_required', ILLEGAL_ROOM_NAME: 'illegal_room_name', BAD_OS_EXIT_ERROR_CODE: 1 }; export default { ACTION_TYPES, SYNC_INFO, SYNC_MESSAGES, SAVE_INFO, CUSTOM_PROXY_HEADERS, ERROR_TYPES }; export { CUSTOM_PROXY_HEADERS, }; ================================================ FILE: backend/src/server/server-reducer.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import redux from 'redux'; import uuid from 'uuid'; import HashRing from 'hashring'; import { logger } from '../logger'; import roomDataConstants from '../rooms/room-data-constants'; import roomNames from '../rooms/room-names'; import { createFilteredActionHandler } from '../utils/reducer-utils'; import config from '../config'; import serverConstants from './server-constants'; const mapServableRooms = ( roomList, hashRing ) => { return roomList.filter( ( roomName ) => { return hashRing.get( roomName ) === config.localIpPortString; } ); }; const initialState = ( () => { let obj = { serverId: config.serverId, syncTopic: null, syncSubscription: null, syncWarmup: { warmupStatus: false, receiptTimes: [] }, appServer: null, websocketServer: null, balancer: null, peers: {}, websockets: {} }; // used to consistently hash room names to target server addresses obj.hashRing = new HashRing( [ `${config.localIpAddress}:${config.serverPort}` ], 'md5', { 'max cache size': 10000 } ); // balancing based on what's available on the server a client arrives at obj.servableRooms = mapServableRooms( roomNames, obj.hashRing ); return obj; } )(); const addSyncSubscriptionWarmupResponse = ( state, action ) => { if( state.syncWarmup.receiptTimes.length >= serverConstants.SYNC_INFO.SYNC_WARMUP_MESSAGE_LIST_LENGTH ) { state.syncWarmup.receiptTimes.shift(); } state.syncWarmup.receiptTimes.push( Date.now() ); return state; }; // action: { syncTopic } const setSyncTopic = ( state, action ) => { if( typeof action.syncTopic === 'undefined' ) { return state; } state.syncTopic = action.syncTopic; return state; }; // action: { syncSubscription } const setSyncSubscription = ( state, action ) => { if( typeof action.syncSubscription === 'undefined' ) { return state; } state.syncSubscription = action.syncSubscription; return state; }; // action: { appServer } const setAppServer = ( state, action ) => { if( typeof action.appServer === 'undefined' ) { return state; } state.appServer = action.appServer; return state; }; // action: { websocketServer } const setWebsocketServer = ( state, action ) => { if( typeof action.websocketServer === 'undefined' ) { return state; } state.websocketServer = action.websocketServer; return state; }; // action: { balancer } const setBalancer = ( state, action ) => { if( typeof action.peerId === 'undefined' ) { return state; } state.balancer = action.balancer; return state; }; // action: { monitorTask, peerId } const setMonitorTaskForPeer = ( state, action ) => { if( typeof action.peerId === 'undefined' ) { return state; } state.peers[ action.peerId ].monitorTask = action.monitorTask; return state; }; // action: { peerId, data } const setLastHeartbeatForPeer = ( state, action ) => { if( typeof action.peerId === 'undefined' ) { return state; } if( !state.peers[ action.peerId ] ) { state.peers[ action.peerId ] = { lastHeartbeatTime: 0, ip: action.data.ip, port: action.data.port }; // add peer to balancer hash ring let peerIpPortString = `${action.data.ip}:${action.data.port}`; logger.info( `adding peer ${peerIpPortString}` ); state.hashRing.add( peerIpPortString ); logger.trace( `balancer hash ring is now:`, Object.keys( state.hashRing.vnodes ) ); // remap list of rooms this server should serve, based on new hash ring let start = Date.now(); state.servableRooms = mapServableRooms( roomNames, state.hashRing ); let end = Date.now(); logger.info( `addPeerToList remapped ${state.servableRooms.length} rooms in ${end - start}ms` ); } state.peers[ action.peerId ].lastHeartbeatTime = Date.now(); return state; }; // action: { serverId } const removePeerFromList = ( state, action ) => { if( typeof action.peerId === 'undefined' ) { return state; } if( state.peers[ action.peerId ] ) { let peer = state.peers[ action.peerId ]; let peerIpPortString = `${peer.ip}:${peer.port}`; // remove peer from balancer hash ring logger.info( `removing peer ${peerIpPortString}` ); state.hashRing.remove( peerIpPortString ); logger.trace( `balancer hash ring is now:`, Object.keys( state.hashRing.vnodes ) ); // remap list of rooms this server should serve, based on new hash ring let start = Date.now(); state.servableRooms = mapServableRooms( roomNames, state.hashRing ); let end = Date.now(); logger.info( `removePeerFromList remapped ${state.servableRooms.length} rooms in ${end - start}ms` ); state.peers[ action.peerId ] = null; delete state.peers[ action.peerId ]; } let remainingPeers = Object.keys( state.peers ); if( remainingPeers.length > 0 ) { logger.info( `remaining peers: `, Object.keys( state.peers )); } else { logger.info( `no remaining peers` ); } return state; }; // action: { ws } const addWebsocketToState = ( state, action ) => { if( typeof action.ws === 'undefined' ) { return state; } state.websockets[ action.ws.id ] = action.ws; let count = Object.keys( state.websockets ).length; logger.trace( `[+] this server now has ${count} websockets` ); return state; }; const setWebsocketTimeoutTaskForClient = ( state, action ) => { if( typeof action.clientId === 'undefined' ) { return state; } state.websockets[ action.clientId ].timeoutTask = action.timeoutTask; return state; }; const removeWebsocketTimeoutTaskFromClient = ( state, action ) => { if( typeof action.clientId === 'undefined' ) { return state; } state.websockets[ action.clientId ].timeoutTask = null; delete state.websockets[ action.clientId ].timeoutTask; return state; }; // action: { ws, roomName } const removeWebsocketFromState = ( state, action ) => { if( typeof action.ws === 'undefined' ) { return state; } if( !state.websockets[ action.ws.id ] ) { return state; } // state.websockets[ action.ws.id ] = null; delete state.websockets[ action.ws.id ]; let count = Object.keys( state.websockets ).length; logger.trace( `[-] this server now has ${count} websockets` ); return state; }; // action: { roomName, wsId } const setQueuedRoomForWebsocket = ( state, action ) => { if( typeof action.wsId === 'undefined' ) { return state; } state.websockets[ action.wsId ].queuedRoom = action.roomName; return state; }; // action: { roomName, wsId } const removeQueuedRoomForWebsocket = ( state, action ) => { if( typeof action.wsId === 'undefined' ) { return state; } state.websockets[ action.wsId ].queuedRoom = null; delete state.websockets[ action.wsId ].queuedRoom; return state; }; // action: { clientId, roomName } const setCurrentRoomForWebsocket = ( state, action ) => { if( typeof action.clientId === 'undefined' ) { return state; } if( !state.websockets[ action.clientId ] ) { return state; } state.websockets[ action.clientId ].currentRoom = action.roomName; state.websockets[ action.clientId ].queuedRoom = null; delete state.websockets[ action.clientId ].queuedRoom; return state; }; // action: { clientId, roomName } const removeCurrentRoomFromWebsocket = ( state, action ) => { if( typeof action.clientId === 'undefined' ) { return state; } if( !state.websockets[ action.clientId ] ) { return state; } state.websockets[ action.clientId ].currentRoom = null; delete state.websockets[ action.clientId ].currentRoom; return state; }; export default createFilteredActionHandler( { [ serverConstants.ACTION_TYPES.ADD_SYNC_SUBSCRIPTION_WARMUP_RESPONSE ]: addSyncSubscriptionWarmupResponse, [ serverConstants.ACTION_TYPES.SET_SYNC_TOPIC ]: setSyncTopic, [ serverConstants.ACTION_TYPES.SET_SYNC_SUBSCRIPTION ]: setSyncSubscription, [ serverConstants.ACTION_TYPES.SET_APP_SERVER ]: setAppServer, [ serverConstants.ACTION_TYPES.SET_WEBSOCKET_SERVER ]: setWebsocketServer, [ serverConstants.ACTION_TYPES.SET_BALANCER ]: setBalancer, [ serverConstants.ACTION_TYPES.SET_MONITOR_TASK_FOR_PEER ]: setMonitorTaskForPeer, [ serverConstants.ACTION_TYPES.SET_LAST_HEARTBEAT_FOR_PEER ]: setLastHeartbeatForPeer, [ serverConstants.ACTION_TYPES.REMOVE_PEER_FROM_LIST ]: removePeerFromList, [ serverConstants.ACTION_TYPES.ADD_WEBSOCKET_TO_STATE ]: addWebsocketToState, [ serverConstants.ACTION_TYPES.SET_WEBSOCKET_TIMEOUT_TASK_FOR_CLIENT ]: setWebsocketTimeoutTaskForClient, [ serverConstants.ACTION_TYPES.REMOVE_WEBSOCKET_TIMEOUT_TASK_FOR_CLIENT ]: removeWebsocketTimeoutTaskFromClient, [ serverConstants.ACTION_TYPES.REMOVE_WEBSOCKET_FROM_STATE ]: removeWebsocketFromState, [ roomDataConstants.ACTION_TYPES.ADD_WEBSOCKET_TO_QUEUE_FOR_ROOM ]: setQueuedRoomForWebsocket, [ roomDataConstants.ACTION_TYPES.ADD_LOCAL_CLIENT_TO_ROOM ]: setCurrentRoomForWebsocket, [ roomDataConstants.ACTION_TYPES.REMOVE_LOCAL_CLIENT_FROM_ROOM ]: removeCurrentRoomFromWebsocket, }, initialState ); ================================================ FILE: backend/src/server/server-sagas.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { delay, takeEvery, takeLatest } from 'redux-saga'; import { call, cancel, cancelled, put, fork, select } from 'redux-saga/effects'; import datastore from '@google-cloud/datastore'; import fetch from 'node-fetch'; import { logger } from '../logger'; import { messageConstants } from '../messages'; import { pubsubClient } from '../pubsub'; import { sendWsMessageWithLogger } from '../utils/websocket-utils'; import { setPerClientRateLimiterOptions, setPerMessageTypeRateLimiterOptions } from '../messages/message-rate-limiter'; import { dsClient, datastoreConstants } from '../datastore'; import { roomDataActions, roomDataConstants, roomStateActions, roomStateConstants } from '../rooms'; import config from '../config'; import { sagaUtils } from '../utils'; import serverActions from './server-actions'; import serverConstants from './server-constants'; import serverSetup from './server-setup'; import serverSync from './server-sync'; import { lbHealthCheckUrl } from './app-server'; /******************************************************************************* * SERVERS SEND SYNC HEARTBEATS UNTIL THEY DIE *******************************************************************************/ const startSendingSyncHeartbeat = function* () { logger.info( `starting to send sync heartbeats every ` + `${serverConstants.SYNC_INFO.SYNC_HEARTBEAT_PERIOD_IN_MS}ms` ); do { try { yield serverSync.sendSyncMessageOfType( // send the local ip and the port of the websocket app server // for remote balancers to add to their lists // and check every SYNC_HEARTBEAT_TIMEOUT_IN_MS { ip: config.localIpAddress, port: config.serverPort }, serverConstants.SYNC_MESSAGES.SYNC_HEARTBEAT ); } catch( error ) { logger.error( `error sending sync heartbeat: ${error.message}` ); } yield delay( serverConstants.SYNC_INFO.SYNC_HEARTBEAT_PERIOD_IN_MS ); } while ( true ); }; /******************************************************************************* * SERVERS CAN LOAD/RELOAD THEIR RATE LIMIT INFO FROM DATASTORE *******************************************************************************/ const loadRateLimiterInfo = function* () { logger.info( `loading rate limiter info for ${config.environmentName}` ); // check if any limit info is in the datastore for this environment let query = dsClient.createQuery( datastoreConstants.DS_ENTITY_KEY_PER_ENVIRONMENT_RATE_LIMIT_INFO ) .filter( 'environmentName', '=', config.environmentName ); let queryResult = yield dsClient.runQuery( query ); if( queryResult[ 0 ].length <= 0 ) { logger.trace( `no rate limiter info stored for ${config.environmentName}` ); return; } let rateLimitInfo = queryResult[ 0 ][ 0 ]; const limitTypes = [ 'perClient_threshold', 'perClient_ttl_millisec', 'perMsgType_threshold', 'perMsgType_ttl_millisec' ]; for( let t of limitTypes ) { if( typeof rateLimitInfo[ t ] !== 'number' ) { logger.trace( `${t} ${rateLimitInfo[ t ]} is not a number, discarding rate limit info` ); return; } } // update the per-client rate limiter let perClientRateLimiterOptions = { threshold: rateLimitInfo.perClient_threshold, ttl_millisec: rateLimitInfo.perClient_ttl_millisec }; setPerClientRateLimiterOptions( perClientRateLimiterOptions ); // update the per-message-type rate limiter let perMessageTypeRateLimiterOptions = { threshold: rateLimitInfo.perMsgType_threshold, ttl_millisec: rateLimitInfo.perMsgType_ttl_millisec }; setPerMessageTypeRateLimiterOptions( perMessageTypeRateLimiterOptions ); }; /******************************************************************************* * SERVERS SAVE THEIR SUBSCRIPTION NAME WITH CURRENT TIMESTAMP UNTIL THEY DIE *******************************************************************************/ const startSavingSubscriptionInfo = function* () { logger.info( `starting to save subscription info every ` + `${serverConstants.SAVE_INFO.SAVE_SUBSCRIPTION_INFO_PERIOD_IN_MS}ms` ); let { roomState, serverState } = yield select( ( state ) => { return { roomState: state.roomStateReducer, serverState: state.serverReducer }; } ); let clientCount = Object.keys( serverState.websockets ).length; let roomsInUse = Object.keys( roomState.roomsByState[ roomStateConstants.STATES.READY ] ).length; const topicName = serverState.syncTopic.name.split( '/' ).pop(); const subscriptionName = serverState.syncSubscription.name.split( '/' ).pop(); const key = dsClient.key( [ datastoreConstants.DS_ENTITY_KEY_SERVER_SUBSCRIPTION_INFO, config.serverId ] ); let data = { serverId: config.serverId, ip: config.localIpAddress, port: config.serverPort, topicName, subscriptionName, clientCount, roomsInUse }; do { ( { roomState, serverState } = yield select( ( state ) => { return { roomState: state.roomStateReducer, serverState: state.serverReducer }; } ) ); data.clientCount = Object.keys( serverState.websockets ).length; data.roomsInUse = Object.keys( roomState.roomsByState[ roomStateConstants.STATES.READY ] ).length + Object.keys( roomState.roomsByState[ roomStateConstants.STATES.ROOM_FULL ] ).length; data.ts = new Date(); let entity = { key, method: 'upsert', data }; try { let result = yield dsClient.save( entity ); } catch( error ) { logger.error( `error saving subscription info to datastore: ${error.message}` ); } yield delay( serverConstants.SAVE_INFO.SAVE_SUBSCRIPTION_INFO_PERIOD_IN_MS ); } while ( true ); }; /******************************************************************************* * SERVERS PERIODICALLY CHECK DATASTORE FOR DEAD PEER SUBSCRIPTION INFO *******************************************************************************/ const startCheckingDeadPeerSubscriptions = function* () { let possiblyDeadPeerPeriod = serverConstants.SAVE_INFO.SAVE_SUBSCRIPTION_INFO_PERIOD_IN_MS * 5; logger.info( `starting to check peer subscriptions every ${possiblyDeadPeerPeriod}ms`); do { try { let deadPeer = yield call( findFirstDeadPeerForCurrentDeployment, possiblyDeadPeerPeriod ); // any results if( typeof deadPeer !== 'undefined' ) { // that aren't somehow from this server if( deadPeer.serverId !== config.serverId ) { // should maybe be deleted yield maybeDeletePeerSubscription( deadPeer ); } else { logger.trace( `not deleting my own sync subscription` ); } } } catch( error ) { logger.error( `error trying to check dead peer subscriptions: ${error.message}` ); } yield delay( serverConstants.SAVE_INFO.CHECK_SUBSCRIPTION_INFO_PERIOD_IN_MS ); } while( true ); }; const findFirstDeadPeerForCurrentDeployment = ( possiblyDeadPeerPeriod ) => { return new Promise( ( resolve, reject ) => { let dateNow = Date.now(); let olderThanDateThreshold = new Date( dateNow - possiblyDeadPeerPeriod ); let query = dsClient.createQuery( datastoreConstants.DS_ENTITY_KEY_SERVER_SUBSCRIPTION_INFO ) .filter( 'ts', '<', olderThanDateThreshold ) .order( 'ts', { descending: true } ); let stream = query.runStream(); let deadPeer; stream .on( 'error', reject ) .on( 'data', ( entity ) => { if( typeof entity[ 'topicName' ] === 'undefined' ) { return; } if( entity.topicName !== config.syncTopicName ) { return; } let timeAgo = new Date( dateNow ) - entity.ts; if( timeAgo < possiblyDeadPeerPeriod ) { return; } deadPeer = entity; stream.end(); } ) .on( 'info', ( info ) => {} ) .on( 'end', () => { resolve( deadPeer ); } ) } ); }; const maybeDeletePeerSubscription = function* ( entity ) { let shouldDeletePeerSubscription = false; try { // first, check against the HTTP endpoint to see if it's still alive let healthCheckUrl = `${'http://'}${entity.ip}:${entity.port}${lbHealthCheckUrl}`; let options = { timeout: 10000 }; logger.info( `health-checking possibly-dead peer: ${entity.topicName} | ${entity.serverId} | ${entity.ip}:${entity.port} | ${entity.ts.toISOString().replace(/^([^T]+)T([^Z]+)Z$/, '$1 $2' )}` ); // fetch() throws on network errors & timeouts let result = yield fetch( healthCheckUrl, options ); logger.trace( `got HTTP status ${result.status} from health-check for peer ${entity.serverId}` ); } catch( error ) { logger.info( `got network/timeout error health-checking peer ${entity.serverId}, marking for deletion` ); shouldDeletePeerSubscription = true; } if( shouldDeletePeerSubscription === false ) { logger.trace( `found peer alive at ${entity.ip}:${entity.port} while checking subscription for peer ${entity.serverId}` ); let peerState = yield select( ( state ) => { return state.serverReducer.peers; } ); // if the peer's IP is alive but the ID isn't in the peer list, // another server must have taken over the IP address, so this // server should carry on and delete the old subscription info if( Object.keys( peerState ).indexOf( entity.serverId ) > 0 ) { return; } logger.info( `IP is responding but server ${entity.serverId} is not in peer list, deleting old subscription info` ); } let topic = yield pubsubClient.getPubsubTopic( entity.topicName ); if( !topic.exists() ) { logger.warn( `not deleting subscription to topic ${entity.topicName}, no such topic exists` ); return; } let subscription = topic.subscription( entity.subscriptionName ); if( !subscription.exists() ) { logger.warn( `not deleting subscription ${entity.subscriptionName}, no such subscription exists` ); return; } try { logger.info( `deleting PubSub subscription ${entity.subscriptionName}` ); let pubsubDeleteResult = yield subscription.delete(); } catch( error ) { // 404 just means another peer deleted this first if( error.code !== 404 ) { // other errors need to be flagged logger.error( `error deleting subscription ${entity.subscriptionName} for supposedly-dead peer ${entity.serverId}: ${error.message}` ); return; } } try { logger.info( `deleting Datastore record for subscription ${entity.subscriptionName}` ); let datastoreDeleteResult = yield dsClient.delete( entity[ datastore.KEY ] ); } catch( error ) { logger.error( `error deleting Datastore record for supposedly-dead peer ${entity.serverId}: ${error.message}` ); } }; const checkMonitorForPeer = function* ( action ) { let serverState = yield select( ( state ) => { return state.serverReducer; } ); // check if a monitor already exists if( typeof serverState.peers[ action.peerId ].monitorTask === 'undefined' ) { try { // create one let task = yield fork( peerMonitorTaskFunction, action.peerId, action.data.ip, action.data.port ); // add to state so next check passes yield put( serverActions.setMonitorTaskForPeerRequestAction( task, action.peerId ) ); } catch( error ) { logger.error( `error trying to create monitor task for peer ${action.peerId}` ); } } }; const peerMonitorTaskFunction = function* ( peerId, ip, port ) { try { logger.info( `starting monitor for peer ${peerId} at ${ip}:${port}` ); do { let peerState = yield select( ( state ) => { return state.serverReducer.peers; } ); let lastHeartbeatTime = peerState[ peerId ].lastHeartbeatTime; // assuming the sync heartbeat has started if( lastHeartbeatTime > 0 ) { // if the last one was too long ago if( ( Date.now() - lastHeartbeatTime ) > serverConstants.SYNC_INFO.SYNC_HEARTBEAT_TIMEOUT_IN_MS ) { try { // first, check against the HTTP endpoint to see if it's still alive let healthCheckUrl = `${'http://'}${ip}:${port}${lbHealthCheckUrl}`; let options = { timeout: 5000 }; // fetch() throws on network errors & timeouts let result = yield fetch( healthCheckUrl, options ); // if it's not returning HTTP 200 OK, throw to trigger removal from peer list if( result.status !== 200 ) { throw new Error( `received HTTP status ${result.status} from health-check` ); } } catch( error ) { // assume it's died logger.warn( `error health-checking peer ${peerId} after monitor timeout: ${error.message}` ); // remove from peer list yield put( serverActions.removePeerFromListRequestAction( peerId ) ); break; } } } // check every half second or so, assuming 5-second timeout yield delay( Math.floor( serverConstants.SYNC_INFO.SYNC_HEARTBEAT_TIMEOUT_IN_MS / 8 ) ); } while ( true ); } catch( error ) { logger.warn( `error starting monitor for peer ${peerId} at ${ip}:${port}` ); } finally { if( yield cancelled() ) { logger.trace( `cancelled monitor for peer ${peerId}` ); } } }; /******************************************************************************* * SAGA WATCHERS *******************************************************************************/ const watchStartServerSetupRequests = function* () { yield takeEvery( serverConstants.ACTION_TYPES.START_SERVER_SETUP, serverSetup.startServerSetup ); }; const watchStartSyncSubscriptionWarmupRequests = function* () { yield takeEvery( serverConstants.ACTION_TYPES.START_SYNC_SUBSCRIPTION_WARMUP, serverSetup.startSyncSubscriptionWarmup ); }; const watchFinishServerSetupRequests = function* () { yield takeEvery( serverConstants.ACTION_TYPES.FINISH_SERVER_SETUP, serverSetup.finishServerSetup ); }; const watchStartSendingSyncHeartbeatRequests = function* () { yield takeEvery( serverConstants.ACTION_TYPES.START_SENDING_SYNC_HEARTBEAT, startSendingSyncHeartbeat ); }; const watchLoadRateLimiterInfoRequests = function* () { yield takeEvery( serverConstants.ACTION_TYPES.LOAD_RATE_LIMITER_INFO_FROM_DATASTORE, loadRateLimiterInfo ); }; const watchStartSavingSubscriptionInfoRequests = function* () { yield takeEvery( serverConstants.ACTION_TYPES.START_SAVING_SUBSCRIPTION_INFO, startSavingSubscriptionInfo ); }; const watchStartCheckingDeadPeerSubscriptionsRequests = function* () { yield takeEvery( serverConstants.ACTION_TYPES.START_CHECKING_DEAD_PEER_SUBSCRIPTIONS, startCheckingDeadPeerSubscriptions ); }; /******************************************************************************* * WHEN SERVERS INTRODUCE THEMSELVES, START MONITORING THEIR HEARTBEATS *******************************************************************************/ const watchPeerHeartbeatEvents = function* () { yield takeEvery( serverConstants.ACTION_TYPES.SET_LAST_HEARTBEAT_FOR_PEER, checkMonitorForPeer ); }; export default { setupServerSaga: function* () { const sagas = [ watchStartServerSetupRequests, watchFinishServerSetupRequests ]; yield sagaUtils.spawnAutoRestartingSagas( sagas ); }, syncSaga: function* () { const sagas = [ watchStartSyncSubscriptionWarmupRequests, watchStartSendingSyncHeartbeatRequests, watchLoadRateLimiterInfoRequests, watchStartSavingSubscriptionInfoRequests, watchStartCheckingDeadPeerSubscriptionsRequests, watchPeerHeartbeatEvents, ]; yield sagaUtils.spawnAutoRestartingSagas( sagas ); } }; ================================================ FILE: backend/src/server/server-setup.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import storage from '@google-cloud/storage'; import { put, select } from 'redux-saga/effects'; import { delay } from 'redux-saga'; import { logger } from '../logger'; import { pubsubClient } from '../pubsub'; import { getStoreState } from '../store'; import config from '../config'; import serverActions from './server-actions'; import serverConstants from './server-constants'; import serverSync from './server-sync'; import serverStartup from './server-startup'; /* * kick off setup, get sync channel topic & subscription */ const startServerSetup = function* ( action ) { logger.info( `connecting to sync channel '${config.syncTopicName}'` ); // get the PubSub sync topic let topic; try { topic = yield getPubsubSyncTopic(); } catch( error ) { logger.error( `pubsub error creating sync channel: ${error.message}` ); process.exit( 1 ); } // create a subscription for the sync topic let subscription; try { subscription = yield getPubsubSyncSubscriptionForTopic( topic ); } catch( error ) { logger.error( `pubsub error subscribing to sync channel: ${error.message}` ); process.exit( 1 ); } // set message & error handlers for the sync subscription setSyncSubscriptionListeners( subscription ); // add sync subscription to server state yield put( serverActions.setSyncSubscriptionRequestAction( subscription ) ); // add sync topic to server state yield put( serverActions.setSyncTopicRequestAction( topic ) ); logger.trace( `connected to sync channel, subscription ${subscription.name}` ); // make sure the pubsub sync topic is warmed up so it doesn't kill monitors yield put( serverActions.startSyncSubscriptionWarmupRequestAction() ); yield put( serverActions.loadRateLimiterInfoRequestAction() ); }; /* * warm up the sync channel Pubsub subscription */ const startSyncSubscriptionWarmup = function* ( action ) { logger.info( `starting sync channel subscription warmup` ); let syncWarmupStartTime = Date.now(); while( true ) { try { yield serverSync.sendSyncMessageOfType( null, serverConstants.SYNC_MESSAGES.SYNC_WARMUP ); } catch( error ) { // this will be a PubSub operational issue, and thus transient }; // small delay period = lots of messages = hopefully warm up faster yield delay( 10 ); let syncIsWarmedUp = yield getSyncWarmedUpStatus(); if( syncIsWarmedUp ) { break; } } // ready to roll let syncWarmupTotalTime = (Date.now() - syncWarmupStartTime) / 1000; logger.trace( `sync channel subscription warmup took ${syncWarmupTotalTime} seconds` ); // sync channel subscription is warmed up, start up the app/websocket servers yield put( serverActions.finishServerSetupRequestAction() ); }; /* * work out whether the sync channel is warmed up yet */ const getSyncWarmedUpStatus = function* () { let receiptTimes = getStoreState().serverReducer.syncWarmup.receiptTimes; // make sure we have enough receipt times to make it worth calculating if( receiptTimes.length < serverConstants.SYNC_INFO.SYNC_WARMUP_MESSAGE_LIST_LENGTH ) { return false; } // work out the differences between the receipt times let differences = []; for( let i = 0; i < receiptTimes.length - 1 ; i++ ) { differences[ i ] = receiptTimes[ i+1 ] - receiptTimes[ i ]; } // 0 difference means they're still being batched let nonZeroDifferences = differences.reduce( ( acc, difference ) => { if( difference == 0 ) { return acc; } acc.push( 1 ); return acc; }, [] ); // fail if less than half the list are still being batched if( nonZeroDifferences.length < ( Math.ceil( serverConstants.SYNC_INFO.SYNC_WARMUP_MESSAGE_LIST_LENGTH ) / 2 ) ) { return false; } // should be warmed up by now return true; } /* * broadcast intro on sync channel, start app/websocket servers */ const finishServerSetup = function* ( action ) { // decide whether to serve SSL, & get certs from storage if so let sslInfo = config.sslInfo.useSsl ? yield getSslCertInfo() : { useSsl: false }; // start sending heartbeats to the sync channel yield put( serverActions.startSendingSyncHeartbeatRequestAction() ); // start saving ID & sync subscription name to datastore // so that peers can clear the subscription if this server dies yield put( serverActions.startSavingSubscriptionInfoRequestAction() ); // start checking the datastore for peer subscription info saved // long enough ago that the peer might be dead yield put( serverActions.startCheckingDeadPeerSubscriptionsRequestAction() ); // create the app server & websocket server let { app, wsServer, balancer } = serverStartup.startServer( sslInfo ); // save the app/websocket server & balancer references in the server state yield put( serverActions.setAppServerRequestAction( app ) ); yield put( serverActions.setWebsocketServerRequestAction( wsServer ) ); yield put( serverActions.setBalancerRequestAction( balancer ) ); }; /* * retrieve SSL certificate data from Cloud Storage */ const getSslCertInfo = function* () { logger.info( `retrieving SSL cert data for ${config.sslInfo.sslCertHostName} ` + `from bucket ${config.sslInfo.sslStorageBucketName}` ); const gcs = storage( { projectId: config.projectId } ); const bucket = gcs.bucket( config.sslInfo.sslStorageBucketName ); const privKeyFile = bucket.file( config.sslInfo.privKeyFileName ); const fullChainFile = bucket.file( config.sslInfo.fullChainFileName ); try { let privKeyResult = yield privKeyFile.download(); let privKeyData = privKeyResult[ 0 ]; let fullChainResult = yield fullChainFile.download(); let fullChainData = fullChainResult[ 0 ]; return { useSsl: true, options: { key: privKeyData, cert: fullChainData } }; } catch( error ) { logger.error( `error retrieving SSL cert data for ${config.sslInfo.sslCertHostName}, ` + `unable to start SSL: got error code ${error.code} (${error.message}) ` + `fetching ${error.response.req.path}` ); process.exit( serverConstants.ERROR_TYPES.BAD_OS_EXIT_ERROR_CODE ); } }; const getPubsubSyncTopic = function* () { // get the sync topic let topic = yield pubsubClient.getPubsubTopic( config.syncTopicName ); // it should already exist, autoCreate only on first server/app boot. // if it throws, let it throw, and be caught & exited above - // this is critical to the app running. let topicResult = yield topic.get( { autoCreate: true } ); topic = topicResult[ 0 ]; return topic; }; const getPubsubSyncSubscriptionForTopic = function* ( topic ) { // create a sync channel subscription for this server let subscriptionName = [ config.syncTopicName, '-', config.serverId ].join( '' ); let subscriptionOptions = { autoAck: true, maxInProgress: 20 }; // subscribe it to the topic let subscriptionResult = yield topic.subscribe( subscriptionName, subscriptionOptions ); let subscription = subscriptionResult[ 0 ]; return subscription; }; const setSyncSubscriptionListeners = ( subscription ) => { // set a message listener let syncChannelMessageHandler = ( message ) => { serverSync.handleSyncMessageWithLocalServerId( message, config.serverId ); }; subscription.on( 'message', syncChannelMessageHandler ); // set error handler let syncChannelErrorHandler = ( error ) => { serverSync.handleSyncErrorWithLocalServerId( error, config.serverId ); }; subscription.on( 'error', syncChannelErrorHandler ); }; export default { startServerSetup, startSyncSubscriptionWarmup, finishServerSetup }; ================================================ FILE: backend/src/server/server-startup.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import http from 'http'; import https from 'https'; import httpProxy from 'http-proxy'; import { FastRateLimit } from 'fast-ratelimit'; import { logger } from '../logger'; import config from '../config'; import { getBalancerFunction } from './balancer'; import app_server from './app-server'; import ws_server from './websocket-server'; import serverConstants from './server-constants'; const errorHandler = ( error ) => { logger.error( `error starting server: ${error.message}` ); process.exit( serverConstants.ERROR_TYPES.BAD_OS_EXIT_ERROR_CODE ); }; const startServer = ( sslInfo ) => { // http server structure for room-server let server = http.createServer(); // create websocket server to run the websocket app let wsServer = ws_server.startWebsocketServer( server ); wsServer.on( 'error', errorHandler ); // create express app to handle HTTP requests let app = app_server.createApp(); app.on( 'error', errorHandler ); // attach express app to plain-http server server.on( 'request', app ); // create balancing proxy server to send room requests to correct nodes let proxyServer; let proxyTimeout = 2000; let balancer; // don't serve non-WS HTTP(S) requests let httpFunction = ( req, res ) => { res.statusCode = 403; res.end(); return; }; // decide whether to serve HTTPS ... if( sslInfo.useSsl ) { logger.info( `serving app over SSL using certs for hostname ${config.sslInfo.sslCertHostName}` ); let proxyOptions = { ssl: sslInfo.options, proxyTimeout }; proxyServer = httpProxy.createProxyServer( proxyOptions ); balancer = https.createServer( sslInfo.options, httpFunction ); } // ... or HTTP else { logger.info( `serving app over plain HTTP` ); let proxyOptions = { proxyTimeout }; proxyServer = httpProxy.createProxyServer( proxyOptions ); balancer = http.createServer( httpFunction ); } // attach error handler to whichever of http/https is being used proxyServer.on( 'error', ( err ) => { logger.error( `proxy error ${err.code} trying to '${err.syscall}' to ${err.address}:${err.port}` ); } ); balancer.on( 'error', ( err ) => { logger.error( `balancer error:`, err ); } ); // add the node-balancing websocket handler to the balancer server balancer.on( 'upgrade', getBalancerFunction( proxyServer ) ); // run the server & the balancer try { server.listen( config.serverPort, () => { logger.info( `starting WS server on port ${config.serverPort}` ); logger.info( `allowing ${config.maxClientsPerRoom} clients per room` ); } ); balancer.listen( config.balancerPort, () => { logger.info( `starting balancer on port ${config.balancerPort}` ); } ); return { app, wsServer, balancer }; } catch( error ) { logger.error( `=> error starting server: ${error.message}` ); process.exit( serverConstants.ERROR_TYPES.BAD_OS_EXIT_ERROR_CODE ); } }; export default { startServer }; ================================================ FILE: backend/src/server/server-sync-handlers.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import serverActions from './server-actions'; import serverConstants from './server-constants'; import { logger } from '../logger'; import { dispatchStoreAction, getStoreState } from '../store'; // new server, possibly this one, has joined & is warming up its sync channel subscription const handleSyncWarmup = ( msgPayload, myId ) => { // not interested in other servers' subscription warmup messages if( msgPayload.from !== myId ) { return; } // update our state with a new warmup response dispatchStoreAction( serverActions.addSyncSubscriptionWarmupResponseRequestAction() ); }; // store peer's last sync heartbeat so as to be able to monitor over time const handleSyncHeartbeat = ( msgPayload, myId ) => { dispatchStoreAction( serverActions.setLastHeartbeatForPeerRequestAction( msgPayload.data, msgPayload.from ) ); }; // handle pubsub request to reload rater limiter info from datastore const handleLoadRateLimiterInfo = () => { dispatchStoreAction( serverActions.loadRateLimiterInfoRequestAction() ); }; export default { handleSyncWarmup, handleSyncHeartbeat, handleLoadRateLimiterInfo }; ================================================ FILE: backend/src/server/server-sync.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { select } from 'redux-saga/effects'; import { logger } from '../logger'; import { serialize, deserialize } from '../s11n'; import config from '../config'; import serverConstants from './server-constants'; import serverSyncHandlers from './server-sync-handlers'; // wrapped in a closure because node.js doesn't seem to parse dots in object literal key definitions const handlerFunctions = ( () => { let m = serverConstants.SYNC_MESSAGES; let obj = {}; obj[ m.SYNC_WARMUP ] = serverSyncHandlers.handleSyncWarmup; obj[ m.SYNC_HEARTBEAT ] = serverSyncHandlers.handleSyncHeartbeat; obj[ m.LOAD_RATE_LIMITER_INFO ] = serverSyncHandlers.handleLoadRateLimiterInfo; return obj; } )(); const handleSyncMessageWithLocalServerId = ( message, localServerId ) => { let msgPayload = deserialize( message.data ); // the only self-sent messages servers care about is warmup let interestingSelfSentMessageTypes = [ serverConstants.SYNC_MESSAGES.SYNC_WARMUP ]; // disregard uninteresting local messages if( interestingSelfSentMessageTypes.indexOf( msgPayload.type ) < 0 ) { if( msgPayload.from === localServerId ) { return; } } if( handlerFunctions[ msgPayload.type ] ) { handlerFunctions[ msgPayload.type ]( msgPayload, localServerId ); } else { logger.trace( `got sync message of type ${msgPayload.type}: `, msgPayload ); } }; const handleSyncErrorWithLocalServerId = ( error, localServerId ) => { logger.error( `server ${localServerId} exiting because of error on sync channel: ${error.message}` ); process.exit( 1 ); }; const sendSyncMessageOfType = function* ( message, type ) { let serverState = yield select( ( state ) => { return state.serverReducer; } ); let syncTopic = serverState.syncTopic; let messageObject = { from: config.serverId, type: type }; if( message ) { messageObject.data = message; } let serializedMessage = serialize( messageObject ); return yield syncTopic.publish( serializedMessage ); }; export default { handleSyncMessageWithLocalServerId, handleSyncErrorWithLocalServerId, sendSyncMessageOfType }; ================================================ FILE: backend/src/server/target-server-utils.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import config from '../config'; const localTargetServer = `${config.localIpAddress}:${config.serverPort}`; const privateTargetServer = `127.0.0.1:${config.serverPort}`; const privatiseTargetServerIfLocal = ( targetServer ) => { if( targetServer === localTargetServer ) { return privateTargetServer; } return targetServer; }; export { privatiseTargetServerIfLocal }; ================================================ FILE: backend/src/server/websocket-server.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Server } from 'uws'; import uuid from 'uuid'; import url from 'url'; import { logger } from '../logger'; import { messageHandler } from '../messages'; import { makeWsReplyMessage, sendWsMessageWithLogger } from '../utils/websocket-utils'; import { roomDataActions, roomStateConstants } from '../rooms'; import { dispatchStoreAction, getStoreState } from '../store'; import config from '../config'; import messageConstants from '../messages/message-constants'; import { perClientRateLimiter } from '../messages/message-rate-limiter'; import { urlUtils } from '../utils'; import serverActions from './server-actions'; import serverConstants from './server-constants'; const CUSTOM_PROXY_HEADERS = serverConstants.CUSTOM_PROXY_HEADERS; const outgoingMsgComponents = messageConstants.OUTGOING_MESSAGE_COMPONENTS; const startWebsocketServer = ( http_server ) => { // create websocket server object let wssConf = { server: http_server }; let wss = new Server( wssConf ); // predefined 'invalid url' message const badUrlMsg = makeWsReplyMessage( config.serverId, messageConstants.ERROR_TYPES.INVALID_URL ); // predefined 'no rooms available' message const noRoomsAvailableMsg = makeWsReplyMessage( config.serverId, messageConstants.ERROR_TYPES.NO_ROOMS_AVAILABLE ); // predefined 'room full' message const roomFullMsg = makeWsReplyMessage( config.serverId, messageConstants.ERROR_TYPES.ROOM_FULL ); // set up an incoming websocket connection wss.on( 'connection', ( ws ) => { let headers = ws.upgradeReq.headers; // if the proxy flagged no rooms available, close the connection if( typeof headers[ CUSTOM_PROXY_HEADERS.X_NO_ROOMS_AVAILABLE ] !== 'undefined' ) { sendWsMessageWithLogger( ws, noRoomsAvailableMsg, logger ); ws.close(); return; } // if the proxy marked the request to be closed, close the connection if( typeof headers[ CUSTOM_PROXY_HEADERS.X_PLEASE_CLOSE_THIS_CONNECTION ] !== 'undefined' ) { sendWsMessageWithLogger( ws, badUrlMsg, logger ); ws.close(); return; } // if some kind of problem stopped a name being specified, close the connection if( typeof headers[ CUSTOM_PROXY_HEADERS.X_REQUESTED_ROOM_NAME ] === 'undefined' ) { sendWsMessageWithLogger( ws, badUrlMsg, logger ); ws.close(); return; } // if this server's running in production ... if( config.projectId === config.productionEnvironmentProjectId ) { // ... and the client didn't provide the correct Origin header let parsedUrl = url.parse( headers.origin ); if( parsedUrl.hostname !== config.productionEnvironmentRequiredOrigin ) { sendWsMessageWithLogger( ws, badUrlMsg, logger ); ws.close(); return; } } // generate UUID to identify connection ws.id = uuid(); ws.lastAction = Date.now(); // get the headset type & optional requested room from custom headers set by proxy ws.headsetType = ws.upgradeReq.headers[ CUSTOM_PROXY_HEADERS.X_CLIENT_HEADSET_TYPE ]; ws.requestedRoomName = ws.upgradeReq.headers[ CUSTOM_PROXY_HEADERS.X_REQUESTED_ROOM_NAME ]; // if the client requested the room directly if( typeof headers[ CUSTOM_PROXY_HEADERS.X_CLIENT_SPECIFIED_ROOM_NAME ] !== 'undefined' ) { let specifiedRoomName = headers[ CUSTOM_PROXY_HEADERS.X_REQUESTED_ROOM_NAME ]; let roomState = getStoreState().roomStateReducer; // belt & braces if( typeof roomState.rooms[ specifiedRoomName ] === undefined ) { sendWsMessageWithLogger( ws, badUrlMsg, logger ); ws.close(); return; } // only MAX_CLIENTS_PER_ROOM allowed into client-specified room names if( roomState.rooms[ specifiedRoomName ].status === roomStateConstants.STATES.ROOM_FULL ) { sendWsMessageWithLogger( ws, roomFullMsg, logger ); ws.close(); return; } // TODO: clients only allowed into rooms with availability for their headset type // check against roomDataReducer.avails[ clientHeadsetType ] // -- empty rooms also allowed ( roomStateReducer.roomsByState ) } // CLIENT: tell the client its id and the id of the server it's connected to let clientMsgData = { [ outgoingMsgComponents.CONNECTION_INFO.CLIENT_ID ]: ws.id, [ outgoingMsgComponents.CONNECTION_INFO.SERVER_ID ]: config.serverId }; let clientMsg = makeWsReplyMessage( config.serverId, messageConstants.OUTGOING_MESSAGE_TYPES.CONNECTION_INFO, clientMsgData ); try{ sendWsMessageWithLogger( ws, clientMsg, logger ); } catch( error ) { logger.trace( `error sending error message to client ${ws.id} while sending room info:`, error.message ); } // add to the server's state, to make accessible via id dispatchStoreAction( serverActions.addWebsocketToStateRequestAction( ws ) ); // route messages from the connection via message-handler/validator/sagas ws.on( 'message', ( message, flags ) => { // drop all viewer messages in production? if( config.dropViewerMessagesInProduction === true ) { if( config.projectId === config.productionEnvironmentProjectId ) { if( ws.headsetType === messageConstants.HEADSET_TYPES.HEADSET_TYPE_VIEWER ) { return; } } } // keep track of last time client did anything for inactivity-timeout ws.lastAction = Date.now(); if( config.rateLimitInfo.perClientRateLimit === true ) { // rate-limit overall msg rate per client perClientRateLimiter.consume( ws.id ) .then( () => { messageHandler.handleWebsocketMessage( ws, message ); }) .catch( () => {} ); } else { messageHandler.handleWebsocketMessage( ws, message ); } } ); // add a handler for when the connection closes ws.on( 'close', () => { if( ws.currentRoom ) { logger.trace( `client ${ws.id} disconnected, leaving room ${ws.currentRoom}` ); dispatchStoreAction( roomDataActions.removeLocalClientFromRoomRequestAction( ws.id, ws.currentRoom ) ); } else { logger.trace( `client ${ws.id} disconnected, not from any room` ); } // remove from the server's state, to make inaccessible via id dispatchStoreAction( serverActions.removeWebsocketFromStateRequestAction( ws ) ); } ); // belt & braces ws.on( 'error', ( error ) => { logger.warn( `websocket error on connection ${ws.id}: ${error.message}` ); } ); } ); return wss; }; export default { startWebsocketServer }; ================================================ FILE: backend/src/spheres/index.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import sphereConstants from './sphere-constants'; import messageConstants from '../messages/message-constants'; const incomingMsgComponents = messageConstants.INCOMING_MESSAGE_COMPONENTS; const toneLabel = incomingMsgComponents.CREATE_SPHERE_OF_TONE_AT_POSITION.TONE; const positionLabel = incomingMsgComponents.CREATE_SPHERE_OF_TONE_AT_POSITION.POSITION; const connectionsLabel = incomingMsgComponents.SET_SPHERE_CONNECTIONS.CONNECTIONS; const makeSphereOfToneAtPosition = ( tone, position ) => { return { [ toneLabel ]: tone, [ positionLabel ]: position, [ connectionsLabel ]: [] }; }; const trees = [ require('./trees/0'), require('./trees/1'), require('./trees/2'), require('./trees/3'), require('./trees/4'), require('./trees/5'), ] export { makeSphereOfToneAtPosition, sphereConstants, trees }; ================================================ FILE: backend/src/spheres/sphere-constants.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const SPHERE_INFO = { DEFAULT_NUMBER_OF_SPHERES_PER_ROOM: 5, MAX_NUMBER_OF_SPHERES_PER_ROOM: 50, MAX_NUMBER_OF_CONNECTIONS_PER_SPHERE: 10, SPHERE_HOLD_TIMEOUT_IN_MS: 5000, MINIMUM_STRIKE_VELOCITY: 0, MAXIMUM_STRIKE_VELOCITY: 127, LOWEST_TONE: 0, HIGHEST_TONE: 17 }; export default { SPHERE_INFO }; ================================================ FILE: backend/src/spheres/trees/0.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import serverMessageConstants from '../../messages/message-constants'; const serverMsgComponents = serverMessageConstants.OUTGOING_MESSAGE_COMPONENTS.ROOM_STATUS_INFO; const toneLabel = serverMsgComponents.TONE; const positionLabel = serverMsgComponents.POSITION; const meristemLabel = serverMsgComponents.MERISTEM; const connectionsLabel = serverMsgComponents.CONNECTIONS; module.exports = { "0": { [ toneLabel ]: 7, [ positionLabel ]: { "x": 0.3, "z": 0, "y": 1.2 }, [ meristemLabel ]: true, [ connectionsLabel ]: [] }, "1": { [ toneLabel ]: 9, [ positionLabel ]: { "x": -0.14999999999999994, "z": 0.2598076211353316, "y": 1.2 }, [ meristemLabel ]: true, [ connectionsLabel ]: [] }, "2": { [ toneLabel ]: 11, [ positionLabel ]: { "x": -0.15000000000000013, "z": -0.2598076211353315, "y": 1.2 }, [ meristemLabel ]: true, [ connectionsLabel ]: [] }, "3": { [ toneLabel ]: 9, [ positionLabel ]: { "x": 3.061616997868383e-17, "z": 0.5, "y": 1.6 }, [ meristemLabel ]: true, [ connectionsLabel ]: [] }, "4": { [ toneLabel ]: 12, [ positionLabel ]: { "x": -0.5, "z": 6.123233995736766e-17, "y": 1.6 }, [ meristemLabel ]: true, [ connectionsLabel ]: [] }, "5": { [ toneLabel ]: 8, [ positionLabel ]: { "x": -9.184850993605148e-17, "z": -0.5, "y": 1.6 }, [ meristemLabel ]: true, [ connectionsLabel ]: [] }, "6": { [ toneLabel ]: 11, [ positionLabel ]: { "x": 0.5, "z": -1.2246467991473532e-16, "y": 1.6 }, [ meristemLabel ]: true, [ connectionsLabel ]: [] }, "7": { [ toneLabel ]: 0, [ positionLabel ]: { "x": 0, "z": 0, "y": 2 }, [ meristemLabel ]: true, [ connectionsLabel ]: [] } } ================================================ FILE: backend/src/spheres/trees/1.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import serverMessageConstants from '../../messages/message-constants'; const serverMsgComponents = serverMessageConstants.OUTGOING_MESSAGE_COMPONENTS.ROOM_STATUS_INFO; const toneLabel = serverMsgComponents.TONE; const positionLabel = serverMsgComponents.POSITION; const meristemLabel = serverMsgComponents.MERISTEM; const connectionsLabel = serverMsgComponents.CONNECTIONS; module.exports = { "0": { [ toneLabel ]: 21, [ positionLabel ]: { "x": 0, "z": 0, "y": 2 }, [ meristemLabel ]: false, [ connectionsLabel ]: [] }, "1": { [ toneLabel ]: 14, [ positionLabel ]: { "x": 0.21, "z": 0, "y": 1.7 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 2 ] }, "2": { [ toneLabel ]: 16, [ positionLabel ]: { "x": -0.10499999999999995, "z": 0.18186533479473213, "y": 1.7 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 1, 3 ] }, "3": { [ toneLabel ]: 18, [ positionLabel ]: { "x": -0.1050000000000001, "z": -0.18186533479473205, "y": 1.7 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 2 ] }, "4": { [ toneLabel ]: 11, [ positionLabel ]: { "x": 0.42, "z": 0, "y": 1.4 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 5 ] }, "5": { [ toneLabel ]: 14, [ positionLabel ]: { "x": -0.2099999999999999, "z": 0.36373066958946426, "y": 1.4 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 4, 6 ] }, "6": { [ toneLabel ]: 16, [ positionLabel ]: { "x": -0.2100000000000002, "z": -0.3637306695894641, "y": 1.4 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 5 ] }, "7": { [ toneLabel ]: 9, [ positionLabel ]: { "x": 0.63, "z": 0, "y": 1.1 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 8 ] }, "8": { [ toneLabel ]: 11, [ positionLabel ]: { "x": -0.31499999999999984, "z": 0.5455960043841964, "y": 1.1 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 7, 9 ] }, "9": { [ toneLabel ]: 14, [ positionLabel ]: { "x": -0.3150000000000003, "z": -0.5455960043841962, "y": 1.1 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 8 ] }, "10": { [ toneLabel ]: 7, [ positionLabel ]: { "x": 0.84, "z": 0, "y": 0.8 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 11 ] }, "11": { [ toneLabel ]: 9, [ positionLabel ]: { "x": -0.4199999999999998, "z": 0.7274613391789285, "y": 0.8 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 10, 12 ] }, "12": { [ toneLabel ]: 11, [ positionLabel ]: { "x": -0.4200000000000004, "z": -0.7274613391789282, "y": 0.8 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 11 ] } } ================================================ FILE: backend/src/spheres/trees/2.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import serverMessageConstants from '../../messages/message-constants'; const serverMsgComponents = serverMessageConstants.OUTGOING_MESSAGE_COMPONENTS.ROOM_STATUS_INFO; const toneLabel = serverMsgComponents.TONE; const positionLabel = serverMsgComponents.POSITION; const meristemLabel = serverMsgComponents.MERISTEM; const connectionsLabel = serverMsgComponents.CONNECTIONS; module.exports = { "0": { [ toneLabel ]: 0, [ positionLabel ]: { "x": 0.3, "z": 0, "y": 1.2 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 1, 2 ] }, "1": { [ toneLabel ]: 2, [ positionLabel ]: { "x": 0.6687355423879241, "z": -0.20686414466293768, "y": 1.3 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 0 ] }, "2": { [ toneLabel ]: 4, [ positionLabel ]: { "x": 0.48296291314453416, "z": 0.12940952255126037, "y": 1.45 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 0, 3, 4 ] }, "3": { [ toneLabel ]: 6, [ positionLabel ]: { "x": 0.7938667524116582, "z": 0.42399950402726544, "y": 1.55 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 2 ] }, "4": { [ toneLabel ]: 8, [ positionLabel ]: { "x": 0.6062177826491071, "z": 0.3499999999999999, "y": 1.7 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 2, 5, 6 ] }, "5": { [ toneLabel ]: 10, [ positionLabel ]: { "x": 1.027104089199748, "z": 0.3937730183102396, "y": 1.8 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 4 ] }, "6": { [ toneLabel ]: 12, [ positionLabel ]: { "x": 0.636396103067893, "z": 0.6363961030678928, "y": 1.95 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 4 ] }, "7": { [ toneLabel ]: 3, [ positionLabel ]: { "x": -0.14999999999999994, "z": 0.2598076211353316, "y": 1.2 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 8, 9 ] }, "8": { [ toneLabel ]: 5, [ positionLabel ]: { "x": -0.15521816678371875, "z": 0.6825740404529765, "y": 1.3 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 7 ] }, "9": { [ toneLabel ]: 7, [ positionLabel ]: { "x": -0.35355339059327373, "z": 0.3535533905932738, "y": 1.45 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 7, 10, 11 ] }, "10": { [ toneLabel ]: 9, [ positionLabel ]: { "x": -0.7641277178854433, "z": 0.4755090227947147, "y": 1.55 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 9 ] }, "11": { [ toneLabel ]: 11, [ positionLabel ]: { "x": -0.6062177826491069, "z": 0.3500000000000002, "y": 1.7 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 9, 12, 13 ] }, "12": { [ toneLabel ]: 13, [ positionLabel ]: { "x": -0.854569481781416, "z": 0.6926117244227405, "y": 1.8 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 11 ] }, "13": { [ toneLabel ]: 15, [ positionLabel ]: { "x": -0.8693332436601615, "z": 0.23293714059226894, "y": 1.95 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 11 ] }, "14": { [ toneLabel ]: 4, [ positionLabel ]: { "x": -0.15000000000000013, "z": -0.2598076211353315, "y": 1.2 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 15, 16 ] }, "15": { [ toneLabel ]: 6, [ positionLabel ]: { "x": -0.5135173756042053, "z": -0.4757098957900386, "y": 1.3 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 14 ] }, "16": { [ toneLabel ]: 8, [ positionLabel ]: { "x": -0.12940952255126076, "z": -0.48296291314453405, "y": 1.45 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 14, 17, 18 ] }, "17": { [ toneLabel ]: 10, [ positionLabel ]: { "x": -0.0297390345262156, "z": -0.8995085268219799, "y": 1.55 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 16 ] }, "18": { [ toneLabel ]: 12, [ positionLabel ]: { "x": -1.2858791391047207e-16, "z": -0.7, "y": 1.7 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 16, 19, 20 ] }, "19": { [ toneLabel ]: 14, [ positionLabel ]: { "x": -0.17253460741833146, "z": -1.0863847427329798, "y": 1.8 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 18 ] }, "20": { [ toneLabel ]: 16, [ positionLabel ]: { "x": 0.2329371405922683, "z": -0.8693332436601617, "y": 1.95 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 18 ] } } ================================================ FILE: backend/src/spheres/trees/3.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import serverMessageConstants from '../../messages/message-constants'; const serverMsgComponents = serverMessageConstants.OUTGOING_MESSAGE_COMPONENTS.ROOM_STATUS_INFO; const toneLabel = serverMsgComponents.TONE; const positionLabel = serverMsgComponents.POSITION; const meristemLabel = serverMsgComponents.MERISTEM; const connectionsLabel = serverMsgComponents.CONNECTIONS; module.exports = { "0": { [ toneLabel ]: 19, [ positionLabel ]: { "x": -0.3801701800236835, "z": 0.22714110856547276, "y": 0.9711673841367696 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 1 ] }, "1": { [ toneLabel ]: 21, [ positionLabel ]: { "x": -0.6427852083066042, "z": 0.009616761652669642, "y": 1.1711673841367696 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 0 ] }, "2": { [ toneLabel ]: 18, [ positionLabel ]: { "x": -0.3717230728255157, "z": -0.35540364805038743, "y": 1.0697267415202358 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 3 ] }, "3": { [ toneLabel ]: 20, [ positionLabel ]: { "x": -0.20030527807571613, "z": -0.6856251725306349, "y": 1.2697267415202358 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 2 ] }, "4": { [ toneLabel ]: 17, [ positionLabel ]: { "x": 0.2775516451627562, "z": -0.5157773829446608, "y": 1.3804880182088852 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 5 ] }, "5": { [ toneLabel ]: 19, [ positionLabel ]: { "x": 0.6683914801829096, "z": -0.4130372477082535, "y": 1.5804880182088852 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 4 ] }, "6": { [ toneLabel ]: 16, [ positionLabel ]: { "x": 0.6406669137194841, "z": 0.14622804231414932, "y": 1.5521284395792219 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 7 ] }, "7": { [ toneLabel ]: 18, [ positionLabel ]: { "x": 0.6283301758541369, "z": 0.5830052038036452, "y": 1.7521284395792218 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 6 ] }, "8": { [ toneLabel ]: 15, [ positionLabel ]: { "x": 0.03268723354108966, "z": 0.7278378056229577, "y": 1.4375443946415316 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 9 ] }, "9": { [ toneLabel ]: 17, [ positionLabel ]: { "x": -0.42773941827558637, "z": 0.8241868040756578, "y": 1.6375443946415316 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 8 ] }, "10": { [ toneLabel ]: 14, [ positionLabel ]: { "x": -0.7608452130361227, "z": 0.24721359549995825, "y": 1.3097886967409693 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 11 ] }, "11": { [ toneLabel ]: 16, [ positionLabel ]: { "x": -0.9781476007338058, "z": -0.2079116908177584, "y": 1.5097886967409693 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 10 ] }, "12": { [ toneLabel ]: 13, [ positionLabel ]: { "x": -0.48006736955111823, "z": -0.7272709782428493, "y": 1.461249175138151 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 13 ] }, "13": { [ toneLabel ]: 15, [ positionLabel ]: { "x": -0.06407587922682341, "z": -1.0695108533225732, "y": 1.6612491751381508 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 12 ] }, "14": { [ toneLabel ]: 12, [ positionLabel ]: { "x": 0.6203422273145859, "z": -0.7100388108034049, "y": 1.7744448880450852 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 15 ] }, "15": { [ toneLabel ]: 14, [ positionLabel ]: { "x": 1.0815170100561873, "z": -0.36938246566224114, "y": 1.9744448880450851 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 14 ] }, "16": { [ toneLabel ]: 11, [ positionLabel ]: { "x": 0.9138398517295967, "z": 0.4400820782478085, "y": 1.8944794878661984 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 17 ] }, "17": { [ toneLabel ]: 13, [ positionLabel ]: { "x": 0.6840314990772564, "z": 1.0032899402408502, "y": 2.0944794878661983 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 16 ] }, "18": { [ toneLabel ]: 10, [ positionLabel ]: { "x": -0.19386177149566014, "z": 1.0682664104785127, "y": 1.7500029067545588 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 19 ] }, "19": { [ toneLabel ]: 12, [ positionLabel ]: { "x": -0.8313423444203109, "z": 0.9807808781086307, "y": 1.9500029067545588 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 18 ] }, "20": { [ toneLabel ]: 9, [ positionLabel ]: { "x": -1.1524845401944908, "z": 0.10372548601683089, "y": 1.6579479983438095 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 21 ] }, "21": { [ toneLabel ]: 11, [ positionLabel ]: { "x": -1.2314153711992457, "z": -0.5704848098486944, "y": 1.8579479983438094 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 20 ] }, "22": { [ toneLabel ]: 8, [ positionLabel ]: { "x": -0.4316890695856524, "z": -1.1502314125002475, "y": 1.8582964637551596 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 23 ] }, "23": { [ toneLabel ]: 10, [ positionLabel ]: { "x": 0.2340255877359067, "z": -1.4092722770336028, "y": 2.0582964637551595 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 22 ] }, "24": { [ toneLabel ]: 7, [ positionLabel ]: { "x": 1.051722092687431, "z": -0.7641208279802159, "y": 2.1618033988749894 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 25 ] }, "25": { [ toneLabel ]: 9, [ positionLabel ]: { "x": 1.4917828430524098, "z": -0.15679269490148218, "y": 2.3618033988749896 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 24 ] } } ================================================ FILE: backend/src/spheres/trees/4.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import serverMessageConstants from '../../messages/message-constants'; const serverMsgComponents = serverMessageConstants.OUTGOING_MESSAGE_COMPONENTS.ROOM_STATUS_INFO; const toneLabel = serverMsgComponents.TONE; const positionLabel = serverMsgComponents.POSITION; const meristemLabel = serverMsgComponents.MERISTEM; const connectionsLabel = serverMsgComponents.CONNECTIONS; module.exports = { "0": { [ toneLabel ]: 7, [ positionLabel ]: { "x": 0.75, "z": 0, "y": 1 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 1 ] }, "1": { [ toneLabel ]: 9, [ positionLabel ]: { "x": 0.9310632489491795, "z": 0.18873586425530814, "y": 1.25 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 0 ] }, "2": { [ toneLabel ]: 8, [ positionLabel ]: { "x": 0.21822143065055674, "z": 0.2736410188638104, "y": 1.1428571428571428 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 3 ] }, "3": { [ toneLabel ]: 12, [ positionLabel ]: { "x": 0.384135321897057, "z": 0.3936242554404447, "y": 1.4928571428571429 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 2 ] }, "4": { [ toneLabel ]: 9, [ positionLabel ]: { "x": -0.16689070046723575, "z": 0.7311959341363677, "y": 1.2857142857142856 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 5 ] }, "5": { [ toneLabel ]: 11, [ positionLabel ]: { "x": -0.3911849258208314, "z": 0.8657218686221058, "y": 1.5357142857142856 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 4 ] }, "6": { [ toneLabel ]: 11, [ positionLabel ]: { "x": -0.6757266509268144, "z": -0.3254128043381685, "y": 1.5714285714285714 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 7 ] }, "7": { [ toneLabel ]: 15, [ positionLabel ]: { "x": -0.8927946788297243, "z": -0.32468086092858855, "y": 1.9214285714285713 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 6 ] }, "8": { [ toneLabel ]: 12, [ positionLabel ]: { "x": -0.0778823268847101, "z": -0.34122476926363826, "y": 1.7142857142857144 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 9 ] }, "9": { [ toneLabel ]: 14, [ positionLabel ]: { "x": -0.01341837989470709, "z": -0.5498362911640168, "y": 1.9642857142857144 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 8 ] }, "10": { [ toneLabel ]: 14, [ positionLabel ]: { "x": 0.35, "z": -8.572527594031472e-17, "y": 2 }, [ meristemLabel ]: true, [ connectionsLabel ]: [] } } ================================================ FILE: backend/src/spheres/trees/5.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import serverMessageConstants from '../../messages/message-constants'; const serverMsgComponents = serverMessageConstants.OUTGOING_MESSAGE_COMPONENTS.ROOM_STATUS_INFO; const toneLabel = serverMsgComponents.TONE; const positionLabel = serverMsgComponents.POSITION; const meristemLabel = serverMsgComponents.MERISTEM; const connectionsLabel = serverMsgComponents.CONNECTIONS; module.exports = { "0": { [ toneLabel ]: 7, [ positionLabel ]: { "x": 0.5, "z": 0, "y": 1.3 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 1 ] }, "1": { [ toneLabel ]: 5, [ positionLabel ]: { "x": 0.8955037487502232, "z": 0.08985007498214534, "y": 1.4000000000000001 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 0 ] }, "2": { [ toneLabel ]: 8, [ positionLabel ]: { "x": 0.11126046697815722, "z": 0.4874639560909118, "y": 1.3357142857142859 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 3 ] }, "3": { [ toneLabel ]: 6, [ positionLabel ]: { "x": 0.1116709845215571, "z": 0.8930451227211232, "y": 1.435714285714286 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 2 ] }, "4": { [ toneLabel ]: 9, [ positionLabel ]: { "x": -0.4504844339512095, "z": 0.21694186955877912, "y": 1.3714285714285714 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 5 ] }, "5": { [ toneLabel ]: 7, [ positionLabel ]: { "x": -0.8458054852071072, "z": 0.3075923945639262, "y": 1.4714285714285715 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 4 ] }, "6": { [ toneLabel ]: 10, [ positionLabel ]: { "x": -0.31174490092936685, "z": -0.39091574123401485, "y": 1.4071428571428573 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 7 ] }, "7": { [ toneLabel ]: 8, [ positionLabel ]: { "x": -0.4880898375488758, "z": -0.756153628888675, "y": 1.5071428571428573 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 6 ] }, "8": { [ toneLabel ]: 11, [ positionLabel ]: { "x": 0.3117449009293667, "z": -0.39091574123401496, "y": 1.4428571428571428 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 9 ] }, "9": { [ toneLabel ]: 9, [ positionLabel ]: { "x": 0.6285850721951838, "z": -0.6441124179934553, "y": 1.542857142857143 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 8 ] }, "10": { [ toneLabel ]: 12, [ positionLabel ]: { "x": 0.45048443395120963, "z": 0.21694186955877895, "y": 1.4785714285714286 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 11 ] }, "11": { [ toneLabel ]: 10, [ positionLabel ]: { "x": 0.7678365122206151, "z": 0.46949663523914764, "y": 1.5785714285714287 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 10 ] }, "12": { [ toneLabel ]: 13, [ positionLabel ]: { "x": -0.11126046697815704, "z": 0.48746395609091187, "y": 1.5142857142857142 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 13 ] }, "13": { [ toneLabel ]: 11, [ positionLabel ]: { "x": -0.2868656765450031, "z": 0.8530580775189798, "y": 1.6142857142857143 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 12 ] }, "14": { [ toneLabel ]: 14, [ positionLabel ]: { "x": -0.5, "z": 1.8369701987210297e-16, "y": 1.55 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 15 ] }, "15": { [ toneLabel ]: 12, [ positionLabel ]: { "x": -0.8955037487502232, "z": -0.08985007498214469, "y": 1.6500000000000001 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 14 ] }, "16": { [ toneLabel ]: 15, [ positionLabel ]: { "x": -0.1112604669781574, "z": -0.48746395609091175, "y": 1.5857142857142859 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 17 ] }, "17": { [ toneLabel ]: 13, [ positionLabel ]: { "x": -0.11167098452155783, "z": -0.8930451227211231, "y": 1.685714285714286 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 16 ] }, "18": { [ toneLabel ]: 16, [ positionLabel ]: { "x": 0.45048443395120946, "z": -0.21694186955877928, "y": 1.6214285714285714 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 19 ] }, "19": { [ toneLabel ]: 14, [ positionLabel ]: { "x": 0.8458054852071069, "z": -0.3075923945639269, "y": 1.7214285714285715 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 18 ] }, "20": { [ toneLabel ]: 17, [ positionLabel ]: { "x": 0.31174490092936696, "z": 0.39091574123401474, "y": 1.6571428571428573 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 21 ] }, "21": { [ toneLabel ]: 15, [ positionLabel ]: { "x": 0.4880898375488761, "z": 0.7561536288886749, "y": 1.7571428571428573 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 20 ] }, "22": { [ toneLabel ]: 18, [ positionLabel ]: { "x": -0.31174490092936585, "z": 0.3909157412340156, "y": 1.6928571428571428 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 23 ] }, "23": { [ toneLabel ]: 16, [ positionLabel ]: { "x": -0.6285850721951823, "z": 0.6441124179934566, "y": 1.792857142857143 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 22 ] }, "24": { [ toneLabel ]: 19, [ positionLabel ]: { "x": -0.4504844339512097, "z": -0.21694186955877878, "y": 1.7285714285714286 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 25 ] }, "25": { [ toneLabel ]: 17, [ positionLabel ]: { "x": -0.7678365122206144, "z": -0.4694966352391487, "y": 1.8285714285714287 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 24 ] }, "26": { [ toneLabel ]: 20, [ positionLabel ]: { "x": 0.11126046697815686, "z": -0.48746395609091187, "y": 1.7642857142857142 }, [ meristemLabel ]: true, [ connectionsLabel ]: [ 27 ] }, "27": { [ toneLabel ]: 18, [ positionLabel ]: { "x": 0.2868656765450043, "z": -0.8530580775189793, "y": 1.8642857142857143 }, [ meristemLabel ]: false, [ connectionsLabel ]: [ 26 ] } } ================================================ FILE: backend/src/store.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { createStore, applyMiddleware, combineReducers } from 'redux'; import createSagaMiddleware from 'redux-saga'; import { logger } from './logger'; import { messageSagas } from './messages'; import { roomSagas } from './rooms'; import { roomDataReducer, roomStateReducer } from './rooms'; import { serverConstants, serverReducer, serverSagas } from './server'; let store = ( () => { let sagaErrorTrap = ( error ) => { logger.error( `sagaMiddleware trapped uncaught error, exiting - error was '${error.message}'` ); process.exit( serverConstants.ERROR_TYPES.BAD_OS_EXIT_ERROR_CODE ); }; let sagaMiddleware = createSagaMiddleware({ onError: sagaErrorTrap }); let reducers = { roomDataReducer, roomStateReducer, serverReducer }; let rootReducer = combineReducers( reducers ); let allMiddleware = applyMiddleware( sagaMiddleware ); let store = createStore( rootReducer, {}, allMiddleware ); // see https://github.com/yelouafi/redux-saga/blob/master/examples/real-world/store/configureStore.prod.js store.runSaga = ( saga ) => { sagaMiddleware.run( saga ); }; // when two sagas listen for the same action, they're run in this order let sagaListsToRun = [ messageSagas, serverSagas, roomSagas ]; sagaListsToRun.forEach( ( sagaList ) => { // run each named saga exported from this saga list Object.keys( sagaList ).forEach( ( sagaName ) => { store.runSaga( sagaList[ sagaName ] ); } ); } ); return store; })(); const getStoreState = store.getState; const dispatchStoreAction = store.dispatch; export { dispatchStoreAction, getStoreState }; ================================================ FILE: backend/src/utils/index.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import reducerUtils from './reducer-utils'; import sagaUtils from './saga-utils'; import stringUtils from './string-utils'; import urlUtils from './url-utils'; export { reducerUtils, sagaUtils, stringUtils, urlUtils }; ================================================ FILE: backend/src/utils/reducer-utils.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import sizeof from 'object-sizeof'; import { handleActions } from 'redux-actions'; const initStateObject = ( roomNames, createFunction, objectLabel, logger ) => { let obj = {}; let start = Date.now(); roomNames.forEach( ( name ) => { obj[ name ] = createFunction(); } ); let finish = Date.now(); let millis = finish - start; let seconds = millis / 1000; let sizeofBytesUsed = sizeof( obj ); let sizeofKbytesUsed = Math.round( sizeofBytesUsed / 1024 ); let sizeofMbytesUsed = sizeofBytesUsed / 1024 / 1024; logger.trace( `initialised ${Object.keys( obj ).length} ${objectLabel} ` + `using ~${sizeofKbytesUsed}KiB ` + `(${sizeofMbytesUsed.toFixed( 2 )}MiB) ` + `in ${seconds} seconds` ); return obj; }; // redux-ignore higher order reducer // adapted from https://github.com/omnidan/redux-ignore const filterActions = ( () => { return function ( reducer ) { var actions = arguments.length <= 1 || arguments[ 1 ] === undefined ? [] : arguments[ 1 ]; var isInList = ( action ) => { return actions.indexOf( action.type ) >= 0; }; var initialState = reducer( undefined, {} ); return function () { var state = arguments.length <= 0 || arguments[ 0 ] === undefined ? initialState : arguments[ 0 ]; var action = arguments[ 1 ]; if ( !isInList( action ) ) { return state; } return reducer( state, action ); }; }; })(); const createFilteredActionHandler = ( actionHandlers, initialState ) => { let actionHandlerNames = Object.keys( actionHandlers ); const actionHandler = filterActions( handleActions( actionHandlers, initialState ), actionHandlerNames ); return actionHandler; } export { initStateObject, filterActions, createFilteredActionHandler }; ================================================ FILE: backend/src/utils/saga-utils.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { call, put, spawn } from 'redux-saga/effects'; import { logger } from '../logger'; /* * https://github.com/redux-saga/redux-saga/pull/644#issuecomment-272236599 * * "Use spawn to start sagas so that an uncaught exception doesn't terminate all of them. * Restart a saga on an asynchronous exception. Terminate it on a sync exception." */ const spawnAutoRestartingSagas = function* ( sagas ) { yield sagas.map( ( saga ) => { let restarter = function* () { let isSyncError = false; let previouslyStarted = false; while (!isSyncError) { isSyncError = true; try { setTimeout( () => { isSyncError = false; } ); if( previouslyStarted ) { logger.info( `restarting crashed saga '${saga.name}'` ); } previouslyStarted = true; yield call( saga ); break; } catch ( error ) { if ( isSyncError ) { throw new Error( `saga '${saga.name}' was terminated because it threw an exception on startup.` ); } logger.error( `uncaught error in saga '${saga.name}': ${error.message}` ); } } } return spawn( restarter ); }) }; export default { spawnAutoRestartingSagas }; ================================================ FILE: backend/src/utils/string-utils.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const formatStringAsGcpResourceName = ( str ) => { let charPattern = /[^A-Za-z0-9-]/gi; let dashPattern = /-{2,}/gi; return str.replace(charPattern, "-").replace(dashPattern, ""); }; export { formatStringAsGcpResourceName }; ================================================ FILE: backend/src/utils/url-utils.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import messageConstants from '../messages/message-constants'; const headsetTypes = Object.values( messageConstants.HEADSET_TYPES ); const getClientInfoFromUrl = ( url ) => { let info = url.split( '/' ); info.shift(); if( info.length === 1 && info[ 0 ] === '' ) { throw new Error( `bad URL: ${url}` ); } if( info.length > 2 ) { throw new Error( `bad URL: ${url}` ); } let clientHeadsetType = info[ 0 ]; if( headsetTypes.indexOf( clientHeadsetType ) < 0 ) { throw new Error( `bad headset type: ${clientHeadsetType}` ); } let roomName = info.length > 1 ? info[ 1 ] : undefined; if( typeof roomName !== 'undefined' && roomName.length !== 4 ) { throw new Error( `bad room name: ${roomName}` ); } return { clientHeadsetType, roomName }; }; export default { getClientInfoFromUrl }; ================================================ FILE: backend/src/utils/websocket-utils.js ================================================ /* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import WebSocket from 'uws'; import { serialize } from '../s11n'; import messageConstants from '../messages/message-constants'; const incomingMsgComponents = messageConstants.INCOMING_MESSAGE_COMPONENTS; const outgoingMsgComponents = messageConstants.OUTGOING_MESSAGE_COMPONENTS; const makeWsReplyMessage = ( from, type, data ) => { return { [ outgoingMsgComponents.ALL_MESSAGES.FROM ]: from, [ outgoingMsgComponents.ALL_MESSAGES.MSG ]: { [ incomingMsgComponents.ALL_MESSAGES.TYPE ]: type, [ incomingMsgComponents.ALL_MESSAGES.DATA ]: data } }; }; const makeWsBroadcastMessage = ( from, type, data ) => { return { [ outgoingMsgComponents.ALL_MESSAGES.TYPE ]: type, [ outgoingMsgComponents.ALL_MESSAGES.FROM ]: from, [ outgoingMsgComponents.ALL_MESSAGES.MSG ]: { [ incomingMsgComponents.ALL_MESSAGES.TYPE ]: type, [ incomingMsgComponents.ALL_MESSAGES.DATA ]: data } }; }; const ackWsSendWithId = ( error, wsId ) => { if( !error ) { return; } }; const sendWsMessageWithLogger = ( ws, msgObj, logger ) => { if( ws.readyState !== WebSocket.OPEN ) { return; } let msg = serialize( msgObj ); // wrap the ack function so we can log the WS id if it errors let ackWsSend = ( ackError ) => { try { ackWsSendWithId( ackError, ws.id ); } // handle this error because otherwise it's stuck in the callback catch( error ) { logger.trace( error.message, msg ); } }; try { ws.send( msg, ackWsSend ); } catch( error ) { logger.trace( `websocket error trying to send message to client id ${ws.id}: ${error.message}` ); throw( error ); } }; const sendWsErrorWithLogger = ( serverId, ws, errType, errMsg ) => { let msgObj = { from: serverId, msg: { type: 'error', data: { errType, detail: errMsg } } }; sendWsMessageWithLogger( ws, msgObj, logger ); }; export { makeWsReplyMessage, makeWsBroadcastMessage, sendWsMessageWithLogger, sendWsErrorWithLogger }; ================================================ FILE: config.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Note, this file is only used on local development. // Running it under appengine, the config.template is used instead var CONFIG = {}; CONFIG.DEFAULT_REGION= "us"; CONFIG.SERVERS = {"us": ""}; ================================================ FILE: config.template ================================================ var CONFIG = {} CONFIG.DEFAULT_REGION = "{{default_region}}"; CONFIG.SERVERS = { {% for server in servers %} "{{server.name}}":"{{server.hostname}}", {% endfor %} }; ================================================ FILE: index.html ================================================ The Musical Forest
headphone icon Headphones Recommended
LOADING
No VR? Take a peek in 360 mode
Friends With Google
?

The Musical Forest

Join users from around the world in a musical forest. Tap or click a shape to play it. If you're using a headset like the HTC VIVE, you can add shapes too. This experiment demonstrates cross-device co-presence in VR, allowing anyone to join in, no matter what device they're using.

Technologies used: AFrame, Three.js, Tone.js, Node.js, PubSub

Made by Google Creative Lab. Check out other WebVR Experiments here and check out the open-source code on GitHub.

about
================================================ FILE: js/ascene.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. let aframeContent = ` `; let div = document.createElement("div"); div.innerHTML = aframeContent; document.body.appendChild(div); ================================================ FILE: js/components/background-objects.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { BgTreeColors, ShadowColor } from '../core/colors'; const DEG2RAD = Math.PI / 180; const HALFPI = Math.PI / 2; const VECTOR_ZERO = new THREE.Vector3( 0, 0, 0 ); const SHADOW_X_AXIS = new THREE.Vector3( 1, 0, 0 ); const SHADOW_Z_AXIS = new THREE.Vector3( 0, 1, 0 ); const SHADOW_LENGTH = 20; const SHADOW_GEOMETRY = new THREE.PlaneGeometry( 0.25, 1 ); const SHADOW_MATERIAL = new THREE.MeshBasicMaterial({ color: ShadowColor, side: THREE.DoubleSide }); AFRAME.registerComponent( 'background-objects', { nLoaded: 0, init: function() { this.treeObjs = [ 'bg-tree-1', 'bg-tree-2', 'bg-tree-3' ]; this.treeGeometry = []; let objLoader = new THREE.OBJLoader(); if ( !AFRAME.utils.device.isMobile() ) { this.treeObjs.forEach( function( obj ) { objLoader.load( '/static/models/' + obj + '.obj', this.checkLoadComplete.bind( this ) ); }, this ); } }, checkLoadComplete: function( objModel ) { this.treeGeometry.push( objModel.children[ 0 ].geometry ); if ( ++this.nLoaded >= this.treeObjs.length ) { this.generateTrees(); } }, generateTrees: function() { let total = AFRAME.utils.device.isMobile() ? 20 : 200; let theta, x, z, mesh, shadow; let treeDistance, treeColor, treeIndex, randomSeed; let perimeter = 8; let materials = [ new THREE.MeshBasicMaterial( { color: new THREE.Color( BgTreeColors[ 0 ] ), side: THREE.DoubleSide } ), new THREE.MeshBasicMaterial( { color: new THREE.Color( BgTreeColors[ 1 ] ), side: THREE.DoubleSide } ), new THREE.MeshBasicMaterial( { color: new THREE.Color( BgTreeColors[ 2 ] ), side: THREE.DoubleSide } ) ]; for ( let i = 0; i < total; i++ ) { theta = DEG2RAD * (i / total) * 360; randomSeed = Math.random(); treeDistance = randomSeed * 18 + perimeter; treeColor = Math.floor( randomSeed * 3 ); treeIndex = Math.floor( i % 3 ); x = Math.cos( theta ) * treeDistance; z = Math.sin( theta ) * treeDistance; // Create and position tree mesh = new THREE.Mesh( this.treeGeometry[ treeIndex ], materials[ treeColor ] ); mesh.position.set( x, 0, z ); mesh.scale.set( 1, 3 + Math.random(), 1 ); mesh.scale.multiplyScalar( 0.0002 ); mesh.lookAt( VECTOR_ZERO ); mesh.rotation.y += HALFPI; mesh.rotation.y += ( Math.random() - 0.5 ) * 0.05; mesh.position.setY( Math.random() * -2 ); // Create and position tree shadow shadow = new THREE.Mesh( SHADOW_GEOMETRY, SHADOW_MATERIAL ); shadow.position.set( x, 0, z ); shadow.lookAt( VECTOR_ZERO ); shadow.rotation.order = 'ZYX'; shadow.rotation.x += HALFPI; shadow.scale.setY( SHADOW_LENGTH ); shadow.translateOnAxis( SHADOW_Z_AXIS, -SHADOW_LENGTH / 2 ); this.el.object3D.add( mesh ); this.el.object3D.add( shadow ); } } }); ================================================ FILE: js/components/ball.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { Note } from '../notes/note'; AFRAME.registerComponent( 'ball', { grow: null, shrink: null, has3dContext: false, schema: { radius: { default: 0.1 }, grabbed: { type: 'boolean', default: false }, hand: { default: 'rightHand' }, // the node that connects to the trunk where other balls grow from meristem: { default: false, type: 'boolean' }, meristemNode: { default: null }, }, init: function() { this.el.addEventListener( 'componentchanged', event => { this.update(); if ( event.detail.name === 'tone' ) { this.updateTone(); } }, false); // the hit animiation this.el.addEventListener( 'hit', event => this.hit( event ) ); this.el.addEventListener( 'highlight', event => this.highlight( event ) ); this.el.addEventListener( 'unHighlight', event => this.unHighlight( event ) ); }, play: function() { this.createShape(); this.updateTone(); document.dispatchEvent( new CustomEvent("BALL_CREATED", { detail: { id: this.el.id, el: this.el, grab: true, hand: this.data.hand } })); // takes a refresh to register the 3d context setTimeout( () => { this.has3dContext = true; }, 0 ); }, createShape: function() { this.note = new Note( this.el.sceneEl.components.palette ); this.note.lightPosition = this.el.sceneEl.components["fake-light"].getLightPosition(); this.note.setTone( this.el.getAttribute( 'tone' ) ); /* * can't replace root mesh of node * due to tight coupling of gaze.js & raycaster * added note as child instead. */ // this.el.setObject3D("mesh", this.note.group) this.el.object3D.add(this.note.group); }, tick: function( t, dt ) { this.note.tick(); }, updateTone: function() { this.note.setTone( this.el.getAttribute( 'tone' ) ); // this.animateTexture(0.5, 0, null); }, hit: function( event ) { this.note.hit( event ); }, highlight: function( event ) { this.note.highlight( event ); }, unHighlight: function( event ) { this.note.unHighlight( event ); } }); ================================================ FILE: js/components/bg-tree-ring-material.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { EnvColors, BgTreeColors } from '../core/colors'; AFRAME.registerComponent( 'bg-tree-ring-material', { schema: { type: 'int', default: 0 }, init: function() { let objLoader = new THREE.OBJLoader(); if ( AFRAME.utils.device.isMobile() ) { objLoader.load( '/static/models/bg-tree-ring.obj', this.onLoaded.bind( this ) ); } }, onLoaded: function( object ) { let mesh = object.children[ 0 ]; let tex = new THREE.Texture( document.getElementById( 'bg-tree-tex' ) ); tex.needsUpdate = true; tex.minFilter = tex.magFilter = THREE.NearestFilter; let shader = THREE.BGTreeShader; let uniforms = THREE.UniformsUtils.clone( shader.uniforms ); uniforms.map.value = tex; uniforms.color.value = new THREE.Color( BgTreeColors[ this.data ] ); uniforms.fogColor.value = new THREE.Color ( EnvColors.fog ); uniforms.fogDensity.value = 0.065; mesh.material = new THREE.ShaderMaterial( { uniforms: uniforms, vertexShader: shader.vertexShader, fragmentShader: shader.fragmentShader, side: THREE.DoubleSide } ); this.el.setObject3D( 'mesh', mesh ); } }); ================================================ FILE: js/components/clicker.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. AFRAME.registerComponent( 'clicker', { raycaster: null, mouse: null, canvas: null, camera: null, intersected: null, teleporter:null, schema: { // radius: { default: 0.1 }, }, init: function() { document.addEventListener("INTRO_COMPLETED", () => { // creates references this.raycaster = new THREE.Raycaster(); this.mouse = new THREE.Vector2(); this.canvas = this.el.sceneEl.canvas; this.camera = this.getCameraFromEntity(document.querySelector("a-entity[camera]")); this.teleporter = document.querySelector("#gaze-floor").components.teleport; // on click // get all balls // filter out the ones out of range // do a check for mouse position // unproject ray to mouse with vector z = -1 // get ray intersects // if intersect length > 1 & if intersect is same as mousedown, hit document.addEventListener("touchstart", (event) => {}); document.addEventListener("touchend", (event) => {}); document.addEventListener("mousedown", (event) => { let entity = this.getIntersectObject({x:event.clientX, y:event.clientY}); if( !entity ) { return; } this.intersected = entity; }); document.addEventListener("mouseup", (event) => { let entity = this.getIntersectObject({x:event.clientX, y:event.clientY}); if( !entity ) { return; } if(this.intersected === entity){ entity.emit("controllerhit", {velocity:0.5}); } this.intersected = null; event.wasSphereHit = true; }); }); }, play: function() { }, pause: function() { }, getIntersectObject: function(intersect) { this.mouse.x = ( intersect.x / document.body.clientWidth ) * 2 - 1; this.mouse.y = - ( intersect.y / document.body.clientHeight ) * 2 + 1; this.raycaster.setFromCamera( this.mouse, this.camera ); let selectables = tree.getElementsByClassName("selectable"); let balls = []; for(let i=0; i 0 ) { return intersects[0].object.parent.parent.el; } else { return null; } }, getCameraFromEntity: function(camera) { for(let i=0; i { this.onUpdated(); }, false); document.addEventListener("SPHERE_TEXTURE_LOADED", (event) => { this.update(); }); }, onLoaded: function() { let mesh = this.el.components["collada-model"].model.children[ 0 ].children[ 0 ]; mesh.material = new THREE.MultiMaterial([ new THREE.MeshBasicMaterial( { color: HeadsetColor } ), new THREE.MeshBasicMaterial( { color: this.data.bandColor } ) ]); this.isLoaded = true; this.onUpdated(); }, update: function( oldData ) { if ( !this.isLoaded) { return; } let mesh = this.el.components["collada-model"].model.children[ 0 ].children[ 0 ]; mesh.material = new THREE.MultiMaterial([ new THREE.MeshBasicMaterial( { color: HeadsetColor } ), new THREE.MeshBasicMaterial( { color: this.data.bandColor } ) ]); }, getColor: function() { return new THREE.Color( this.el.sceneEl.components.palette.controllerColors()[0] ); }, onUpdated: function() { if(!this.isLoaded) { return; } let mesh = this.el.components["collada-model"].model.children[ 0 ].children[ 0 ]; let transparent = (this.data.opacity<1) ? true : false; mesh.material.materials[0].transparent = transparent; mesh.material.materials[1].transparent = transparent; mesh.material.materials[0].opacity = this.data.opacity; mesh.material.materials[1].opacity = this.data.opacity; }, }); ================================================ FILE: js/components/controllers.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const getComponentProperty = AFRAME.utils.entity.getComponentProperty; const setComponentProperty = AFRAME.utils.entity.setComponentProperty; AFRAME.registerSystem('controllers', { controllerExists:false, schema: { }, init: function() { document.addEventListener("INTRO_COMPLETED", (event) => { let timeout; let loopCount = 0; let loopFunction = () => { if (loopCount > 240 ) { } else if(this.controllerExists){ } else { timeout = setTimeout(() => { loopCount++; this.testForController(); loopFunction(); }, 1000); } }; loopFunction(); }, false); }, getGamePad: function() { let vrGamepad = null; if (!navigator.getGamepads) { return vrGamepad; } let gamepads = navigator.getGamepads(); for (let i = 0; i < gamepads.length; ++i) { let gamepad = gamepads[i]; if (gamepad) { return gamepad; } } return null; }, testForController: function() { let vrGamepad = this.getGamePad(); if (!vrGamepad) { return; } let player; let controller; let rightHand; let leftHand; switch(vrGamepad.id){ case "Daydream Controller": player = document.querySelector('#player'); controller = this.createDayDreamCoprescenceController("rightHand", "right"); player.appendChild(controller); this.createDayDreamManager(); break; case "OpenVR Gamepad": player = document.querySelector('#player'); rightHand = this.createViveController("rightHand", "right"); player.appendChild(rightHand); leftHand = this.createViveController("leftHand", "left"); player.appendChild(leftHand); break; case "Oculus Touch (Left)": case "Oculus Touch (Right)": player = document.querySelector('#player'); rightHand = this.createOcculusController("rightHand", "right"); player.appendChild(rightHand); leftHand = this.createOcculusController("leftHand", "left"); player.appendChild(leftHand); break; default: player = document.querySelector('#player'); rightHand = this.createOcculusController("rightHand", "right"); player.appendChild(rightHand); leftHand = this.createOcculusController("leftHand", "left"); player.appendChild(leftHand); break; } let triggerEvent = new CustomEvent("CONTROLLER_CREATED", { "detail": { "id": vrGamepad.id } }); document.dispatchEvent(triggerEvent); this.controllerExists = true; }, createViveController: function(id, hand) { let controller = document.createElement('a-entity'); controller.id = id; if(this.sceneEl.components.palette && this.sceneEl.components.palette.controllerColors()) { let color = this.sceneEl.components.palette.controllerColors()[1]; controller.setAttribute("vive-controls", "hand:" + hand + "; model:false"); this.load6DofControllerParams(controller, hand, color); } else { document.addEventListener("SPHERE_TEXTURE_LOADED", (event) => { let color = this.sceneEl.components.palette.controllerColors()[1]; controller.setAttribute("vive-controls", "hand:" + hand + "; model:false"); this.load6DofControllerParams(controller, hand, color); }); } return controller; }, load6DofControllerParams: (entity, hand, color) => { entity.setAttribute("mixin", "avatar-hand"); entity.setAttribute("grab-move", ""); entity.setAttribute("tool-tips", "hand:"+hand+";"); entity.setAttribute("haptics", "hand:"+ hand ); entity.setAttribute("touch-color", ""); entity.setAttribute("controller-material", "bandColor:"+color+";"); entity.setAttribute("copresence", "components: mixin, position, rotation; decimals:3; playerpart:"+hand); }, createOcculusController: function(id, hand) { let controller = document.createElement('a-entity'); controller.id = id; if(this.sceneEl.components.palette && this.sceneEl.components.palette.controllerColors()) { let color = this.sceneEl.components.palette.controllerColors()[1]; controller.setAttribute("oculus-touch-controls", "hand:" + hand + "; model:false"); this.load6DofControllerParams(controller, hand, color); } else { document.addEventListener("SPHERE_TEXTURE_LOADED", (event) => { let color = this.sceneEl.components.palette.controllerColors()[1]; controller.setAttribute("oculus-touch-controls", "hand:" + hand + "; model:false"); this.load6DofControllerParams(controller, hand, color); }); } return controller; }, /* * Proxy controller. used for copresence */ createDayDreamCoprescenceController: function(id, hand) { let controller = document.createElement('a-entity'); controller.id = id; controller.setAttribute("position", "0 0 0"); controller.setAttribute("copresence", "components: mixin, position, rotation; decimals:3; playerpart:"+hand); return controller; }, createDayDreamManager: function() { let player = document.querySelector('#player'); let avatar = document.querySelector('#avatar'); let avatarPosition = getComponentProperty(avatar, "position"); let manager = document.createElement('a-entity'); manager.id = "daydream"; manager.setAttribute("daydream-manager", "headset: #avatar; proxy: #rightHand; controller: #controller;"); manager.setAttribute("position", avatarPosition.x + " 0.0 " + avatarPosition.z); this.sceneEl.appendChild(manager); let controller = document.createElement('a-entity'); controller.id = "controller"; controller.setAttribute("daydream-controller", ""); controller.setAttribute("position", "0 0.1 0"); controller.setAttribute("daydream-pointer", ""); controller.setAttribute("mixin", "avatar-hand"); if(this.sceneEl.components.palette && this.sceneEl.components.palette.controllerColors()) { let color = this.sceneEl.components.palette.controllerColors()[1]; controller.setAttribute("controller-material", "bandColor:"+color+";"); } else { document.addEventListener("SPHERE_TEXTURE_LOADED", (event) => { let color = this.sceneEl.components.palette.controllerColors()[1]; controller.setAttribute("controller-material", "bandColor:"+color+";"); }); } manager.appendChild(controller); }, doesControllerExists: function() { return this.controllerExists; } }); ================================================ FILE: js/components/copresence-server-messages.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import messageConstants from '../../backend/src/messages/message-constants'; const clientMsgTypes = messageConstants.INCOMING_MESSAGE_TYPES; const clientMsgComponents = messageConstants.INCOMING_MESSAGE_COMPONENTS; const serverMsgComponents = messageConstants.OUTGOING_MESSAGE_COMPONENTS; class Message { constructor(type){ this.data = {}; this.type = type; } serialize(){ let msg = JSON.stringify({ [ clientMsgComponents.ALL_MESSAGES.TYPE ]: this.type, [ clientMsgComponents.ALL_MESSAGES.DATA ]: this.data }); return msg; } } export class ExitRoom extends Message { constructor(){ super( clientMsgTypes.EXIT_ROOM ); } } export class RoomClientPositionUpdate extends Message { constructor(playerData, spheresData){ super( clientMsgTypes.UPDATE_CLIENT_COORDS ); const clientParts = clientMsgComponents.UPDATE_CLIENT_COORDS; const coordParts = clientMsgComponents.REF_COORDINATE_SET; this.data[clientParts.HEAD] = {}; this.data[clientParts.LEFT] = {}; this.data[clientParts.RIGHT] = {}; this.data[clientParts.HEAD][coordParts.POSITION] = playerData['head']['position']; this.data[clientParts.HEAD][coordParts.ROTATION] = playerData['head']['rotation']; this.data[clientParts.LEFT][coordParts.POSITION] = playerData['left']['position']; this.data[clientParts.LEFT][coordParts.ROTATION] = playerData['left']['rotation']; this.data[clientParts.RIGHT][coordParts.POSITION] = playerData['right']['position']; this.data[clientParts.RIGHT][coordParts.ROTATION] = playerData['right']['rotation']; this.data[clientParts.SPHERES] = [] for(let sphere of spheresData){ let s = {}; s[clientMsgComponents.UPDATE_CLIENT_COORDS.SPHERE_ID] = sphere.id; s[clientMsgComponents.UPDATE_CLIENT_COORDS.SPHERE_POSITION] = sphere.position; this.data[clientParts.SPHERES].push(s) } } } export class SpherePositionUpdate extends Message { constructor(uuid, position){ super( clientMsgTypes.UPDATE_SPHERE_POSITION ); this.data[ clientMsgComponents.UPDATE_SPHERE_POSITION.SPHERE_ID ] = uuid; this.data[ clientMsgComponents.UPDATE_SPHERE_POSITION.POSITION ] = position; } } export class SphereToneUpdate extends Message { constructor(uuid, tone){ super( clientMsgTypes.SET_SPHERE_TONE ); this.data[ clientMsgComponents.SET_SPHERE_TONE.SPHERE_ID ] = uuid; this.data[ clientMsgComponents.SET_SPHERE_TONE.TONE ] = tone; } } export class SphereConnectionUpdate extends Message { constructor(uuid, connections){ super( clientMsgTypes.SET_SPHERE_CONNECTIONS ); this.data[ clientMsgComponents.SET_SPHERE_CONNECTIONS.SPHERE_ID ] = uuid; this.data[ clientMsgComponents.SET_SPHERE_CONNECTIONS.CONNECTIONS ] = connections } } export class GrabSphere extends Message { constructor(uuid){ super( clientMsgTypes.GRAB_SPHERE ); this.data[ clientMsgComponents.GRAB_SPHERE.SPHERE_ID ] = uuid; } } export class ReleaseSphere extends Message { constructor(uuid){ super( clientMsgTypes.RELEASE_SPHERE ); this.data[ clientMsgComponents.RELEASE_SPHERE.SPHERE_ID ] = uuid; } } export class StrikeSphere extends Message { constructor(uuid, velocity=1){ super( clientMsgTypes.STRIKE_SPHERE ); this.data[ clientMsgComponents.STRIKE_SPHERE.SPHERE_ID ] = uuid; this.data[ clientMsgComponents.STRIKE_SPHERE.VELOCITY ] = velocity; } } export class DeleteSphere extends Message { constructor(uuid){ super( clientMsgTypes.DELETE_SPHERE ); this.data[ clientMsgComponents.DELETE_SPHERE.SPHERE_ID ] = uuid; } } export class CreateSphereAtPosition extends Message { constructor(position, tone=1){ super( clientMsgTypes.CREATE_SPHERE_OF_TONE_AT_POSITION ); this.data[ clientMsgComponents.CREATE_SPHERE_OF_TONE_AT_POSITION.POSITION ] = position; this.data[ clientMsgComponents.CREATE_SPHERE_OF_TONE_AT_POSITION.TONE ] = tone; } } ================================================ FILE: js/components/copresence-server.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import serverMessageConstants from '../../backend/src/messages/message-constants'; import { ExitRoom, RoomClientPositionUpdate, CreateSphereAtPosition, DeleteSphere, SpherePositionUpdate, SphereToneUpdate, SphereConnectionUpdate, GrabSphere, ReleaseSphere, StrikeSphere } from './copresence-server-messages'; import _ from 'underscore'; import { getParameterByName, getViewerType, showErrorMessage } from '../util'; const msgTypes = serverMessageConstants.OUTGOING_MESSAGE_TYPES; const errorTypes = serverMessageConstants.ERROR_TYPES; const clientMsgComponents = serverMessageConstants.INCOMING_MESSAGE_COMPONENTS; const serverMsgComponents = serverMessageConstants.OUTGOING_MESSAGE_COMPONENTS; const getComponentProperty = AFRAME.utils.entity.getComponentProperty; const setComponentProperty = AFRAME.utils.entity.setComponentProperty; const stringify = AFRAME.utils.coordinates.stringify; const MESSAGE_THROTTLE_INTERVAL = 200; /// /// ------------------------------- SYSTEM -------------------------------------- /// AFRAME.registerSystem('copresence-server', { // Holds the local players data (head and arm location) playerDataCache: { head: { position: {x:0, y:1.6, z:0}, rotation: {x:0, y:0, z:0}}, left: { position: {x:0.25, y:-10001.3, z:0}, rotation: {x:0, y:0, z:0}}, right: { position: {x:-0.25, y:-10001.3, z:0}, rotation: {x:0, y:0, z:0}}, userdata: {} }, playerDataDirty: true, entities: {}, sphereCache: {}, connected: false, throttledSendMsg: null, schema: { hostname: {type: 'string'}, port: {type: 'number'} }, init: function(){ this.throttledServerUpdate = _.throttle(()=>{ this.sendClientUpdate(); }, MESSAGE_THROTTLE_INTERVAL, {leading:true, trailing:true}); // this.throttledSendMsg = _.throttle((msg)=>{ // this.sendMsg(msg); // }, MESSAGE_THROTTLE_INTERVAL, {leading:true, trailing:true}); }, tick: function(){ // Check if player data has changed if(this.playerDataDirty && this.connected){ this.throttledServerUpdate(); } }, sendClientUpdate: function () { this.playerDataDirty = false; let spheres = []; for(let sphereId in this.sphereCache){ if(this.sphereCache[sphereId].dirty){ this.sphereCache[sphereId].dirty = false; spheres.push({ id: sphereId, position: this.sphereCache[sphereId].position }); } } let playerMsg = new RoomClientPositionUpdate(this.playerDataCache, spheres); this.sendMsg(playerMsg); }, //returns a promise which is resolved when the server connects connectToServer: function(){ if(this.connected) { return; } this.region = CONFIG.DEFAULT_REGION; this.roomname = undefined; if(window.location.hash){ var matches = window.location.hash.match(/#(\w+)-(\w{4})/); if(matches.length >= 3){ this.region = matches[1]; this.roomname = matches[2]; } } // If server doesnt exist, pick another available server if(!CONFIG.SERVERS[this.region]){ this.region = Object.keys(CONFIG.SERVERS)[0]; } let ws_url = [ "wss://",CONFIG.SERVERS[this.region], ":", "443" ].join(''); if(getParameterByName("server")){ ws_url = getParameterByName("server"); } // client type is asynchronous let connectWithClientType = (clientType) => { ws_url = ws_url + '/' + clientType; if(this.roomname){ ws_url = ws_url + '/' + this.roomname; } return new Promise((connected, error) => { // window.UI.connecting(); this.ws = new WebSocket( ws_url ); this.ws.onopen = connected; this.ws.onmessage = (msg) => { let msg_data; try { msg_data = JSON.parse(msg.data); } catch (e) { console.error(e); showErrorMessage("error_shake.gif", "Hmm, something went wrong", "Home"); } if(msg_data) { this.parseWebsocketMessage( msg_data[ clientMsgComponents.ALL_MESSAGES.MSG ], msg_data[ clientMsgComponents.ALL_MESSAGES.FROM ] ); } }; this.ws.onclose = () => { // window.UI.connectionError(); }; this.ws.onerror = (err) =>{ if(this.ws.readyState > 1){ // TODO: Error handling error(err); showErrorMessage("error_shake.gif", "Hmm, something went wrong", "Home"); } }; }); }; getViewerType((clientType) => { connectWithClientType(clientType); }); }, parseWebsocketMessage: function(msg, from){ if(!msg) return; let msgType = msg[ clientMsgComponents.ALL_MESSAGES.TYPE ]; let msgData = msg[ clientMsgComponents.ALL_MESSAGES.DATA ]; switch( msgType ){ // Room case msgTypes.CONNECTION_INFO: this.clientId = msgData[ serverMsgComponents.CONNECTION_INFO.CLIENT_ID ]; this.serverId = msgData[ serverMsgComponents.CONNECTION_INFO.SERVER_ID ]; break; case msgTypes.ROOM_STATUS_INFO: this.handleRoomStatusInfo(msgData); break; case msgTypes.ROOM_EXIT_SUCCESS: break; case msgTypes.ROOM_HEARTBEAT: break; case msgTypes.ROOM_CLIENT_JOIN: if(!this.connected) break; this.handleRemoteJoining(msgData); break; case msgTypes.ROOM_CLIENT_EXIT: if(!this.connected) break; this.handleRemoveEntity(msgData); break; // Remote Clients case msgTypes.ROOM_CLIENT_COORDS_UPDATED: if(!this.connected) break; this.handleUpdatePlayerEntity(from, msgData); if(msgData[ serverMsgComponents.ROOM_CLIENT_COORDS_UPDATED.SPHERES ]){ for(let sphere of msgData[ serverMsgComponents.ROOM_CLIENT_COORDS_UPDATED.SPHERES ]) { this.handleSpherePositionUpdate(sphere); } } break; // Spheres case msgTypes.ROOM_SPHERE_CREATED: if(!this.connected) break; this.handleSphereCreation(msgData, true, msgTypes.ROOM_SPHERE_CREATED); break; case msgTypes.ROOM_SPHERE_GRABBED: break; case msgTypes.ROOM_SPHERE_POSITION_UPDATED: // Deprecated if(!this.connected) break; this.handleSpherePositionUpdate(msgData); break; case msgTypes.ROOM_SPHERE_TONE_SET: if(!this.connected) break; this.handleSphereToneUpdate(msgData); break; case msgTypes.ROOM_SPHERE_CONNECTIONS_SET: if(!this.connected) break; this.handleSphereConnectionUpdate(msgData); break; case msgTypes.ROOM_SPHERE_RELEASED: break; case msgTypes.ROOM_SPHERE_STRUCK: if(!this.connected) break; this.handleSphereStruck(msgData); break; case msgTypes.ROOM_SPHERE_DELETED: if(!this.connected) break; this.handleSphereDeletion(msgData, true); break; case msgTypes.CREATE_SPHERE_DENIED: break; case msgTypes.CREATE_SPHERE_SUCCESS: if(!this.connected) break; this.handleSphereCreation(msgData, false, msgTypes.CREATE_SPHERE_SUCCESS); break; case msgTypes.GRAB_SPHERE_DENIED: break; case msgTypes.GRAB_SPHERE_SUCCESS: break; case msgTypes.UPDATE_SPHERE_POSITION_INVALID: break; case msgTypes.UPDATE_SPHERE_POSITION_DENIED: break; case msgTypes.RELEASE_SPHERE_DENIED: break; case msgTypes.RELEASE_SPHERE_INVALID: break; case msgTypes.RELEASE_SPHERE_SUCCESS: break; case msgTypes.DELETE_SPHERE_DENIED: break; case msgTypes.DELETE_SPHERE_INVALID: break; case msgTypes.DELETE_SPHERE_SUCCESS: break; case msgTypes.SET_SPHERE_TONE_DENIED: break; case msgTypes.SET_SPHERE_TONE_INVALID: break; case msgTypes.SET_SPHERE_TONE_SUCCESS: break; case msgTypes.SET_SPHERE_CONNECTIONS_DENIED: break; case msgTypes.SET_SPHERE_CONNECTIONS_INVALID: break; case msgTypes.SET_SPHERE_CONNECTIONS_SUCCESS: break; case msgTypes.CONNECT_SPHERES_IDENTICAL: break; case msgTypes.CONNECT_SPHERES_INVALID: break; case msgTypes.CONNECT_SPHERES_MISSING: break; case errorTypes.INVALID_URL: showErrorMessage("error_shake.gif", "Invalid URL", "Try Again"); break; case errorTypes.NO_ROOMS_AVAILABLE: showErrorMessage("error_full.gif", "All rooms are currently occupied. Try again later", "Home"); break; case errorTypes.NO_SUCH_ROOM: showErrorMessage("error_shake.gif", "Hmm, something went wrong", "Home"); break; case errorTypes.ROOM_QUEUE_FULL: showErrorMessage("error_full.gif", "There are too many players in this room", "Try Another"); break; case errorTypes.ROOM_FULL: showErrorMessage("error_full.gif", "There are too many players in this room", "Try Another"); break; case errorTypes.ROOM_UNAVAILABLE: showErrorMessage("error_knock.gif", "This room is temporarily unavailable", "Home"); break; case errorTypes.BUSY_TRY_AGAIN: showErrorMessage("error_hit.gif", "Hmm, something went wrong", "Try Again"); break; case errorTypes.ROOM_NOT_READY: showErrorMessage("error_knock.gif", "Hmm, something went wrong", "Try Again"); break; case errorTypes.ROOM_JOIN_TIMEOUT: showErrorMessage("error_knock.gif", "Hmm, something went wrong", "Try Again"); break; case errorTypes.SPHERE_HOLD_TIMEOUT: break; case errorTypes.CLIENT_INACTIVITY_TIMEOUT: break; default: } }, sendMsg: function(msg){ if(this.ws && this.ws.readyState == 1) { this.ws.send(msg.serialize()); } }, handleRoomStatusInfo: function(data){ this.connected = true; const roomName = data[ serverMsgComponents.ROOM_STATUS_INFO.ROOM_NAME ]; const roomSoundbank = data[ serverMsgComponents.ROOM_STATUS_INFO.SOUNDBANK ]; // list of clients already in the room arranged according to headsetType const clientList = data[ serverMsgComponents.ROOM_STATUS_INFO.CLIENTS ]; Object.keys( clientList ).forEach(( headsetType ) => { for(let i = 0; i { document.removeEventListener("SPHERE_TEXTURE_LOADED", loadSpheres); // Create spheres // if empty room, trigger tree created event if(Object.keys(data[serverMsgComponents.ROOM_STATUS_INFO.SPHERES]).length===0){ this.sceneEl.systems.tree.isTreeCreated = true; document.dispatchEvent(new Event("TREE_CREATED")); } else { _.each( data[serverMsgComponents.ROOM_STATUS_INFO.SPHERES], (sphereData, sphereId) =>{ sphereData[ serverMsgComponents.ROOM_SPHERE_POSITION_UPDATED.SPHERE_ID ] = sphereId; this.handleSphereCreation(sphereData, true,"PER_SPHERE"); } ); } }; // texture needs to be loaded before spheres are created // window.UI.joinedRoom(roomName); window.location.hash = this.region+"-"+roomName; this.sceneEl.removeAttribute('palette'); this.sceneEl.setAttribute('palette', roomSoundbank); document.addEventListener("SPHERE_TEXTURE_LOADED", loadSpheres); this.sceneEl.components.palette.loadSphereTextures(); }, // ENTITY handleRemoveEntity: function(data){ let clientId = data[ serverMsgComponents.ROOM_CLIENT_EXIT.CLIENT_ID ]; let entity = this.entities[clientId]; if(entity) { entity.parentNode.removeChild(entity); } }, handleRemoveAllEntities: function(){ for (let key in this.entities) { let entity = this.entities[key]; if(entity) { entity.parentNode.removeChild(entity); } } }, createAframeEntity: function(id){ let entity = document.createElement('a-entity'); entity.id = id; this.entities[id] = entity; return entity; }, // Create new player entity representing remote player createRemotePlayerEntity: function(id, headsetType="6dof"){ const playerEntity = this.createAframeEntity(id); setComponentProperty(playerEntity, 'smooth-motion', 'amount:3'); this.sceneEl.appendChild(playerEntity); switch (headsetType){ case "viewer": this.addHeadPart(playerEntity, 'head_' + id, 'avatar-head'); break; case "3dof": this.addHeadPart(playerEntity, 'head_' + id, 'avatar-head'); this.add3dofPart(playerEntity, 'right_' + id, 'avatar-hand'); break; case "6dof": this.addHeadPart(playerEntity, 'head_' + id, 'avatar-head'); this.add6dofPart(playerEntity, 'right_' + id, 'avatar-hand'); this.add6dofPart(playerEntity, 'left_' + id, 'avatar-hand'); break; default: console.warn(clientHeadset); } let triggerEvent = new CustomEvent("PLAYER_ADDED", { "detail": { "entity": playerEntity } }); document.dispatchEvent(triggerEvent); }, // a la carte remote player parts addHeadPart: function(playerEntity, id, partType) { const entity = this.createAframeEntity(id); playerEntity.appendChild(entity); setTimeout( () => { setComponentProperty(entity, 'position', '0 1.6 0'); setComponentProperty(entity, 'smooth-motion', 'amount:3'); setComponentProperty(entity, 'mixin', partType); }); }, add6dofPart: function(playerEntity, id, partType) { const entity = this.createAframeEntity(id); playerEntity.appendChild(entity); setTimeout( () => { setComponentProperty(entity, 'smooth-motion', 'amount:3'); setComponentProperty(entity, 'mixin', partType); }); }, add3dofPart: function(playerEntity, id, partType, callback=null) { const entity = this.createAframeEntity(id); playerEntity.appendChild(entity); setTimeout( () => { setComponentProperty(entity, 'smooth-motion', 'amount:3'); setComponentProperty(entity, 'daydream-pointer', ""); setComponentProperty(entity, 'mixin', partType); }); }, // Handle client position data from server handleUpdatePlayerEntity: function(id, data){ if(!this.entities[id]) { this.createRemotePlayerEntity(id); } // Update position and rotation of head and hands ['head','left', 'right'].forEach((part) => { let dataPart; if(part == 'head') dataPart = serverMsgComponents.ROOM_CLIENT_COORDS_UPDATED.HEAD; else if(part == 'left') dataPart = serverMsgComponents.ROOM_CLIENT_COORDS_UPDATED.LEFT; else if(part == 'right') dataPart = serverMsgComponents.ROOM_CLIENT_COORDS_UPDATED.RIGHT; const part_id = part + "_" + id; let positionData = data[dataPart][clientMsgComponents.REF_COORDINATE_SET.POSITION]; let rotationData = data[dataPart][clientMsgComponents.REF_COORDINATE_SET.ROTATION]; if(part === 'head') { setComponentProperty(this.entities[id], 'position', {x:positionData.x, y:0, z:positionData.z}); setComponentProperty(this.entities[part_id], 'position',{x:0, y:positionData.y, z:0}); } else { let playerPos = getComponentProperty(this.entities[id], 'position'); if(!playerPos) { return; } if(!this.entities[part_id]) { return; } setComponentProperty(this.entities[part_id], 'position', { x:positionData.x-playerPos.x, y:positionData.y-playerPos.y, z:positionData.z-playerPos.z }); } setComponentProperty(this.entities[part_id], 'rotation', stringify(rotationData)); }); }, // Set client position data on server in next tick setPlayerData: function(part, component, data){ if(!this.connected) return; if(component === 'position' && part === 'head') { let avatar = document.querySelector("#avatar"); let avatarPosition = avatar.object3D.getWorldPosition(); this.playerDataCache[part][component].x = avatarPosition.x; this.playerDataCache[part][component].y = avatarPosition.y; this.playerDataCache[part][component].z = avatarPosition.z; } else if(component === 'position') { let hand = document.querySelector("#"+part+"Hand"); let handPosition = hand.object3D.getWorldPosition(); this.playerDataCache[part][component].x = handPosition.x; this.playerDataCache[part][component].y = handPosition.y; this.playerDataCache[part][component].z = handPosition.z; } else if(component == 'rotation') { this.playerDataCache[part][component].x = data.x; this.playerDataCache[part][component].y = data.y; this.playerDataCache[part][component].z = data.z; } else { // this.playerData[part].userdata[component] = data; } this.playerDataDirty = true; }, // SPHERES createSphereOnServer: function(entity){ if(!this.connected) return; this._addingSphereEntity = entity; let msg = new CreateSphereAtPosition(entity.getAttribute('position'), entity.getAttribute('tone')); this.sendMsg(msg); }, setSphereData: function(uuid, component, data){ if(!this.connected) return; if(!uuid) throw new Error("No uuid in setSphereData"); if(component == 'position'){ if(!_.isEqual(data, this.sphereCache[uuid].position)) { this.sphereCache[uuid].dirty = true; // let msg = new SpherePositionUpdate(uuid, data); // this.throttledSendMsg(msg) this.sphereCache[uuid].position = _.clone(data); this.playerDataDirty = true; } } else if(component == 'ball'){ if(data.grabbed != this.sphereCache[uuid].grabbed) { this.sphereCache[uuid].grabbed = data.grabbed; if (data.grabbed) { this.sendMsg(new GrabSphere(uuid)); } else { this.sendMsg(new ReleaseSphere(uuid)); } } } else if(component == 'tone'){ if(!_.isEqual(data, this.sphereCache[uuid].tone)) { let msg = new SphereToneUpdate(uuid, data); this.sendMsg(msg); this.sphereCache[uuid].tone = _.clone(data); } } }, removeSphere: function(uuid){ if(!this.connected) return; if(uuid){ this.sendMsg(new DeleteSphere(uuid)); } }, sendSphereStrike: function(uuid, velocity){ if(!this.connected) return; if(uuid) { this.sendMsg(new StrikeSphere(uuid, velocity)); } }, handleSphereStruck: function (data) { const sphereId = data[ serverMsgComponents.ROOM_SPHERE_STRUCK.SPHERE_ID ]; const strikeVelocity = data[ serverMsgComponents.ROOM_SPHERE_STRUCK.VELOCITY ]; let entity = document.querySelector("#sphere_"+sphereId); if(!entity) { return; } entity.emit("controllerhit", {"velocity":strikeVelocity, "remote": true}); }, handleSphereCreation: function(data, remote, message){ const sphereId = data[ serverMsgComponents.ROOM_STATUS_INFO.SPHERE_ID ]; const spherePosition = data[ serverMsgComponents.ROOM_STATUS_INFO.POSITION ]; const sphereTone = data[ serverMsgComponents.ROOM_STATUS_INFO.TONE ]; const sphereMeristem = data[ serverMsgComponents.ROOM_STATUS_INFO.MERISTEM ]; this.sphereCache[sphereId] = { grabbed: false, connections: [], tone: sphereTone }; if(!remote && this._addingSphereEntity){ // Local client created sphere, getting server generated UUID this._addingSphereEntity.id = "sphere_"+sphereId; this._addingSphereEntity.setAttribute('copresence','uuid', sphereId); delete this._addingSphereEntity; } else if(spherePosition){ // Create entity in tree system document.querySelector('a-scene').systems.tree.createSphere({ id: "sphere_"+sphereId, uuid: sphereId, position: spherePosition, tone: sphereTone, meristem : sphereMeristem || false }); } }, handleSphereDeletion: function(data, remote=true){ const sphereId = data[ serverMsgComponents.ROOM_SPHERE_DELETED.SPHERE_ID ]; let sphere = document.querySelector("#sphere_"+sphereId); document.querySelector('a-scene').systems.tree.deleteSphere(sphere); }, handleSpherePositionUpdate: function(data){ const sphereId = data[ serverMsgComponents.ROOM_SPHERE_POSITION_UPDATED.SPHERE_ID ]; const spherePosition = data[ serverMsgComponents.ROOM_STATUS_INFO.POSITION ]; this.sphereCache[sphereId].position = _.clone(spherePosition); let entity = document.querySelector("#sphere_"+sphereId); if(!entity) { return; } let ballComponent = entity.getAttribute('ball'); if(!ballComponent.grabbed) { entity.setAttribute('position', spherePosition); } }, handleSphereToneUpdate: function(data){ const sphereId = data[ serverMsgComponents.ROOM_STATUS_INFO.SPHERE_ID ]; const sphereTone = data[ serverMsgComponents.ROOM_STATUS_INFO.TONE ]; this.sphereCache[sphereId].tone = _.clone(sphereTone); let entity = document.querySelector("#sphere_"+sphereId); if(!entity) { return; } entity.setAttribute('tone', sphereTone); }, handleSphereConnectionUpdate: function (data) { const sphereId = data[ serverMsgComponents.ROOM_STATUS_INFO.SPHERE_ID ]; const sphereConnections = data[ serverMsgComponents.ROOM_STATUS_INFO.CONNECTIONS ]; let connections = _.map(sphereConnections, (c)=> "#sphere_"+c ); this.sphereCache[sphereId].connections = connections.slice(); let entity = document.querySelector("#sphere_"+sphereId); if(!entity) { return; } let ballComponent = entity.getAttribute('ball'); if(ballComponent && !ballComponent.grabbed) { entity.setAttribute('ball', 'connections', connections); } }, handleRemoteJoining: function(data){ let clientId = data[ serverMsgComponents.ROOM_CLIENT_JOIN.CLIENT_ID ]; let clientHeadset = data[ serverMsgComponents.ROOM_CLIENT_JOIN.CLIENT_HEADSET_TYPE ]; // checks if controllers exists, delete / append based on interaction type let addPointer = () => { let right = document.querySelector("#right_" + clientId); let left = document.querySelector("#left_" + clientId); if(right && left){ let person = right.parentNode; switch (clientHeadset){ case "viewer": person.removeChild(right); person.removeChild(left); break; case "3dof": right.setAttribute("daydream-pointer", ""); person.removeChild(left); break; case "6dof": break; default: } } else { requestAnimationFrame(addPointer); } }; addPointer(); }, }); /// /// ------------------------------- COMPONENT -------------------------------------- /// AFRAME.registerComponent('copresence', { uuidset: false, schema: { components: {default: ['position','ball', 'tone'], type: 'array'}, decimals: {default: 3}, playerpart: {default: undefined, type: 'string'}, uuid: {default: undefined, type: 'string'} }, init: function(){ const system = this.el.sceneEl.systems["copresence-server"]; const player = document.querySelector("#player"); const avatar = document.querySelector("#avatar"); if (!this.data.components.length) return; // sphere is created locally, need to create one for the server if(!this.data.uuid && !this.data.playerpart){ system.createSphereOnServer(this.el); } else if(this.data.uuid){ this.uuidset = true; } let updatePlayerData = (evt) => { this.data.components.forEach( (component) => { // Check the component is in the list of watched components if (evt.detail.name === component) { let d = evt.detail.newData; // Round data if (this.data.decimals !== false) { if (_.isObject(d)) { d = _.mapObject(d, (n) => { if (_.isNumber(n)) { return Math.round(n * Math.pow(10, this.data.decimals)) / Math.pow(10, this.data.decimals); } return n; }); } if (_.isNumber(d)) { d = Math.round(d * Math.pow(10, this.data.decimals)) / Math.pow(10, this.data.decimals); } } // Let the system know about the data change if (this.data.playerpart) { system.setPlayerData(this.data.playerpart, component, d); } else if(this.data.uuid){ system.setSphereData(this.data.uuid, component, d); } } }); }; // Listen for component changes (position, etc) this.el.addEventListener("componentchanged", (evt)=>{ if(_.isEqual(evt.detail.newData, evt.detail.oldData)) { return; } updatePlayerData(evt); }); player.addEventListener("componentchanged", (evt)=>{ // if data has updated and if it's avatar if(_.isEqual(evt.detail.newData, evt.detail.oldData)) { return; } if(!_.isEqual(avatar, this.el)) { return; } updatePlayerData(evt); }); this.el.addEventListener('controllerhit', (evt)=>{ if(!evt.detail.remote) { let system = this.el.sceneEl.systems["copresence-server"]; system.sendSphereStrike(this.data.uuid, evt.detail.velocity); } }); }, update: function(){ // Check if UUID is being set for the first time, if thats the case, send the sphere data if(!this.uuidset && this.data.uuid){ this.uuidset = true; let system = this.el.sceneEl.systems["copresence-server"]; system.setSphereData(this.data.uuid, 'ball', this.el.getAttribute("ball")); } }, remove: function(){ let system = this.el.sceneEl.systems["copresence-server"]; if(this.data.uuid) { system.removeSphere(this.data.uuid); } } }); ================================================ FILE: js/components/daydream-manager.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const getComponentProperty = AFRAME.utils.entity.getComponentProperty; const setComponentProperty = AFRAME.utils.entity.setComponentProperty; AFRAME.registerComponent('daydream-manager', { controllerBall: null, headset: null, proxy: null, controller: null, isHitByRayCast:false, schema: { headset: { default: "#avatar" }, proxy: { default: "#rightHand" }, controller: { default: "#controller" }, }, init: function() { }, play: function() { this.headset = document.querySelector(this.data.headset); this.proxy = document.querySelector(this.data.proxy); this.controller = document.querySelector(this.data.controller); let controller = document.querySelector("#controller"); controller.addEventListener('raycaster-intersection', (event) => { if(this.isHitByRayCast) { return; } let proxyPosition = this.proxy.object3D.getWorldPosition(); event.detail.intersections.forEach((intersection) => { let entity = intersection.object.el; if(!entity.classList.contains('ball')){ return;} let distance = entity.object3D.getWorldPosition().distanceToSquared (proxyPosition); if(distance > 1) { return; } entity.emit("controllerhit", { velocity: 1.0 }); this.isHitByRayCast = true; }); }); controller.addEventListener('raycaster-intersection-cleared', (event) => { this.isHitByRayCast = false; }); }, pause: function() { }, tick: function() { let controller = document.querySelector("#controller"); if(!controller) { return; } let rotation = getComponentProperty(controller, "rotation"); setComponentProperty(this.proxy, "rotation", {x:rotation.x, y:rotation.y, z:rotation.z}); let avatar = document.querySelector('#avatar'); avatar = avatar.object3D; let position = getComponentProperty(controller, "position"); setComponentProperty(this.proxy, "position", { x:position.x + avatar.position.x, y:position.y, z:position.z + avatar.position.z}); }, remove: function() { // let prevEntity = document.querySelector(prevId); }, }); ================================================ FILE: js/components/daydream-pointer.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const getComponentProperty = AFRAME.utils.entity.getComponentProperty; const setComponentProperty = AFRAME.utils.entity.setComponentProperty; AFRAME.registerComponent('daydream-pointer', { schema: { }, init: function() { }, play: function() { let cone = document.createElement('a-cone'); cone.className = "ray"; cone.setAttribute("position", "0 0 -0.44"); cone.setAttribute("rotation", "-90 0 0"); cone.setAttribute("radius-bottom", "0.001"); cone.setAttribute("radius-top", "0.005"); cone.setAttribute("height", "1.0"); cone.setAttribute("color", "#FFF"); cone.setAttribute("shader", "flat"); this.el.appendChild(cone); let ball = document.createElement('a-sphere'); ball.className = "ball"; ball.setAttribute("segments-width", "8"); ball.setAttribute("segments-height", "8"); ball.setAttribute("position", "0 0.50 0"); ball.setAttribute("radius", "0.01"); cone.setAttribute("color", "#FFF"); cone.setAttribute("shader", "flat"); cone.appendChild(ball); }, pause: function() { }, tick: function() { }, }); ================================================ FILE: js/components/fake-light.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. AFRAME.registerComponent( 'fake-light', { schema: { position: { type: 'vec3', default: new THREE.Vector3( 3, 10, 1 ) } }, getLightPosition: function() { return this.data.position; } }); ================================================ FILE: js/components/ga.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. AFRAME.registerComponent( 'ga', { init: function(){ this.el.addEventListener('controllerhit', ()=>{ ga('send', 'event', "interaction", "hit"); }); this.el.addEventListener('grab', ()=>{ ga('send', 'event', "interaction", "grab"); }); } }); ================================================ FILE: js/components/gaze.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const getComponentProperty = AFRAME.utils.entity.getComponentProperty; const setComponentProperty = AFRAME.utils.entity.setComponentProperty; let BALL_RADIUS = 0.05; AFRAME.registerComponent('gaze', { isRayCasterAvailable: false, hoveredEntity: null, schema: { }, // gaze should only work for android, ios, desktop // if vive, occulous, and daydream, disable gaze; init: function() { document.addEventListener("INTRO_COMPLETED", (event) => { let mode = this.el.sceneEl.components.splash.mode; if( AFRAME.utils.device.isMobile() && mode !== "360" ){ this.addRaycaster(); } document.addEventListener("CONTROLLER_CREATED", (event) => { if(event.detail.id !== "Daydream Controller") { return; } this.createRayTargets(); this.removeShadows(); let avatar = document.querySelector('#avatar'); let controller = document.querySelector('#controller'); // swaps out racaster if(avatar.hasAttribute('raycaster')) { avatar.removeAttribute("raycaster"); } let target = document.createElement('a-entity'); target.id = "target"; setComponentProperty(target, "position", "0 0 0.25"); controller.appendChild(target); target.setAttribute("raycaster", "objects: .selectable; near:0; far:10; recursive: true; interval:200"); }, false); }, false); document.addEventListener("BALL_CREATED", (event) => { if (AFRAME.utils.device.isMobile()) { this.createRayTarget(event.detail.el); } }, false); }, play: function() { }, pause: function() { }, tick: function() { }, // required to redo hit states addRaycaster: function() { this.createRayTargets(); let avatar = document.querySelector('#avatar'); // deletes old rays and adds new one if(avatar.hasAttribute('raycaster')) { avatar.removeAttribute("raycaster"); } avatar.setAttribute("raycaster", "objects: .selectable; near:0; far:10; recursive: true; interval:200"); /* * the raycaster intersection updates continuously on interval, * returning an array ofhit objects * Intersection clear not required */ // null state is gaze-floor let currentEntity = document.querySelector('#gaze-floor'); avatar.addEventListener('raycaster-intersection', (event) => { if(event.detail.intersections.length === 0 ) { return; } let entity = event.detail.intersections[0].object.el; let isBall = entity.classList.contains('ball'); if(isBall && (currentEntity.id !== entity.id) ){ currentEntity.emit("unHighlight"); currentEntity = entity; this.hoveredEntity = entity; this.hoveredEntity.emit("highlight"); // teleport related event.wasSphereHit = true; document.dispatchEvent(new Event("ON_SPHERE_IN")); event.stopPropagation(); } else if(isBall && (currentEntity.id === entity.id) ){ } else if (this.hoveredEntity) { currentEntity = document.querySelector('#gaze-floor'); this.hoveredEntity.emit("unHighlight"); this.hoveredEntity = null; // teleport related document.dispatchEvent(new Event("ON_SPHERE_OUT")); event.stopPropagation(); } }); avatar.addEventListener('raycaster-intersection-cleared', (event) => { if(this.hoveredEntity){ currentEntity = document.querySelector('#gaze-floor'); this.hoveredEntity.emit("unHighlight"); this.hoveredEntity = null; } }); document.addEventListener("mouseup", (event) => { if(this.hoveredEntity) { this.hoveredEntity.emit("controllerhit", {velocity:1.0}); event.wasSphereHit = true; } }); }, /* * create ray targets, remove shadows for performance */ createRayTargets: function() { let tree = document.querySelector("#tree"); let balls = tree.getElementsByClassName("selectable"); for(let i=0; i { // when ball is created, makes sure hand is passed to correct ball if(this.data.grabbing && event.detail.grab && event.detail.hand===this.el.id) { let closestSphere = event.detail.el; this.selectBall(closestSphere); this.grabbedSphere = closestSphere; closestSphere.emit("controllerhit", {velocity:0.5}); this.hasJustPlayed = true; } }, false); }, selectBall: function(entity){ if(this.selectedSphere && this.selectedSphere !== -1) { this.selectedSphere.setAttribute('ball', 'grabbed', false); } if( entity === -1 ){ this.selectedSphere = -1; } else { this.selectedSphere = entity; this.selectedSphere.setAttribute('ball', 'grabbed', true); } }, // 57, 48 onKeyUp: function (event) { switch(event.keyCode){ // close bracket case 219: this.onTriggerPress(); break; // open bracket case 221: this.onTriggerRelease(); break; // close parenthisis case 48: let controllerPos = this.controllerBall.getWorldPosition(); let [ closestSphere, distanceSquared ] = this.getClosestEntity(tree, controllerPos); this.grabbedSphere = closestSphere; if(this.grabbedSphere){ this.delete(); } break; } }, creatControllerBall: function() { let material, geometry, ball; geometry = new THREE.IcosahedronGeometry( 0.04, 1); material = new THREE.MeshBasicMaterial( { color: 0xffffff, transparent:true, opacity: 0.25 }); this.controllerBall = new THREE.Mesh(geometry,material); // ball = new THREE.Object3D(); this.controllerBall.name = "ball"; this.el.object3D.add(this.controllerBall); if(this.el.id === "avatar") { this.controllerBall.position.z = -1.0; } else { this.controllerBall.position.z = -0.03; } this.el.object3D.add(this.controllerBall); // delete from avatar document.addEventListener("CONTROLLER_CREATED", (event) => { if(this.el.id !== "avatar") { return; } if(!this.controllerBall) { return; } this.el.object3D.remove(this.controllerBall); }, false); }, play: function () { this.creatControllerBall(); this.enableInteractions(); }, enableInteractions: function() { if (this.el.id === "avatar" && getParameterByName('reticle')==='true') { document.addEventListener('keyup', this.onKeyUp.bind(this)); } this.el.addEventListener('triggerdown', this.onTriggerPress.bind(this)); this.el.addEventListener('triggerup', this.onTriggerRelease.bind(this)); }, pause: function () { if (this.el.id === "avatar" && getParameterByName('reticle')==='true') { document.removeEventListener('keyup', this.onKeyUp); } this.el.removeEventListener('triggerdown', this.onTriggerPress); this.el.removeEventListener('triggerup', this.onTriggerRelease); }, getClosestEntity: function(tree, pos, ignored) { let minValue = 100000; let closestId = -1; for(let i=0; i DELETE_VELOCITY) { this.delete(); } }, delete: function () { if(!this.grabbedSphere) { return; } let controllerPos = this.controllerBall.getWorldPosition(); let triggerEvent = new CustomEvent("ON_DELETE",{ "detail": { "closestEntity": this.grabbedSphere, }}); document.dispatchEvent(triggerEvent); }, getVelocity: function(controllerPos) { let velocity; // compute velocity if (this.prevControllerPos){ let distance = this.prevControllerPos.distanceTo(controllerPos); let elapsedTime = Date.now() - this.lastUpdate; let instantVelocity = distance / elapsedTime; this.lastUpdate = Date.now(); this.prevControllerPos.copy(controllerPos); this.velocityHistory.push(distance); } else { this.prevControllerPos = controllerPos.clone(); this.velocityHistory.push(0); } //keep no more than 10 if (this.velocityHistory.length > VELOCITY_SAMPLES){ this.velocityHistory.shift(); } //compute the velocity as an average over all of the velocity history velocity = this.velocityHistory.reduce(function(a, b) { return a + b; }) / this.velocityHistory.length; velocity = Math.min(velocity / 0.02, 1); // normalize return velocity; }, tick: function(){ let tree = document.querySelector("#tree"); if(!this.controllerBall || tree.children.length === 0) { return; } let controllerPos = this.controllerBall.getWorldPosition(); let [ closestSphere , distanceSquared ] = this.getClosestEntity(tree, controllerPos); if(!closestSphere) { return; } if(closestSphere.id === "TEMP_NEW_SPHERE_ID") { return; } let hitScalar = (this.getShape(closestSphere) === "sphere") ? 1 : 1.5; let thresholdSquared = this.getScale(closestSphere) * BALL_RADIUS*hitScalar + CONTROLLER_BALL_RADIUS; thresholdSquared = thresholdSquared*thresholdSquared; this.data.isNearSphere = distanceSquared < thresholdSquared; let velocity = this.getVelocity(controllerPos); // hightlight controller if near a ball if(this.data.isNearSphere) { this.previousClosestSphere = this.data.closestSphere; this.data.closestSphere = closestSphere; this.controllerBall.material.opacity = 0.75; } else { this.controllerBall.material.opacity = 0.25; this.hasJustPlayed = false; } // repositions ball when grabbing so ball rests just above controller. if(this.data.grabbing && (this.selectedSphere !== -1)) { let treePosition = tree.object3D.getWorldPosition(); // controllerPos = this.controllerBall.getWorldPosition(); let matrix = new THREE.Matrix4(); matrix.extractRotation( this.el.object3D.matrix ); // let tone = this.getTone(this.selectedSphere); let tone = this.getTone(this.selectedSphere) % this.getTonesInScale(); let hitScalar = (this.getShape(this.selectedSphere) === "sphere") ? 1 : 1.5; let scale = 1 - tone / (this.getTotalTones() - 1); let ballSize = 0.1; ballSize *= scale*hitScalar; ballSize += 0.05; let direction = new THREE.Vector3( 0, 0, 1 ); direction.applyMatrix4( matrix ); direction.normalize(); direction.multiplyScalar(ballSize-0.05); controllerPos.sub(treePosition); controllerPos.sub(direction); // if ball position is too low if (controllerPos.y < 0.25) { controllerPos.y = 0.25; } setComponentProperty(this.selectedSphere, "position", AFRAME.utils.coordinates.stringify(controllerPos)); } // touch hits if(this.data.grabbing) { return; } if(!this.data.isNearSphere) { return; } if(!this.hasJustPlayed || this.previousClosestSphere !== closestSphere) { closestSphere.emit("controllerhit", {velocity:velocity, controllerPosition: this.controllerBall.getWorldPosition()}); this.hasJustPlayed = true; this.el.emit('vibrate'); } }, getTone(el){ return el.getAttribute('tone'); }, getTotalTones(el){ return this.el.sceneEl.components.palette.totalNotes(); }, getTonesInScale() { return this.el.sceneEl.components.palette.noteCount(); }, getScale(entity) { return entity.components.ball.note.head.scale; }, getShape(entity) { return entity.components.ball.note.head.shape; }, }); ================================================ FILE: js/components/haptics.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. AFRAME.registerComponent('haptics', { schema: { hand: {type:'string'} }, init: function(){ this.el.addEventListener('vibrate', ()=>{ try { let gamepad = navigator.getGamepads()[this.el.getAttribute("tracked-controls").controller]; if ('hapticActuators' in gamepad && gamepad.hapticActuators.length > 0) { gamepad.hapticActuators[0].pulse(0.7, 10); } } catch (e){} }); } }); ================================================ FILE: js/components/headset-material.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { HeadsetColor, HeadsetShadow } from '../core/colors'; AFRAME.registerComponent( 'headset-material', { isLoaded: false, schema: { opacity: { default: 1.0 }, }, init: function() { this.el.addEventListener( 'model-loaded', this.onLoaded.bind( this ) ); this.el.addEventListener("componentchanged", (event) => { this.onUpdated(); }, false); }, onLoaded: function() { let mesh = this.el.components["collada-model"].model.children[ 0 ].children[ 0 ]; mesh.material = new THREE.MultiMaterial([ new THREE.MeshBasicMaterial( { color: HeadsetColor, transparent:true} ), new THREE.MeshBasicMaterial( { color: HeadsetShadow, transparent:true} ) ]); this.isLoaded = true; this.onUpdated(); }, onUpdated: function() { if(!this.isLoaded) { return; } let mesh = this.el.components["collada-model"].model.children[ 0 ].children[ 0 ]; let transparent = (this.data.opacity<1) ? true : false; mesh.material.materials[0].transparent = transparent; mesh.material.materials[1].transparent = transparent; mesh.material.materials[0].opacity = this.data.opacity; mesh.material.materials[1].opacity = this.data.opacity; }, }); ================================================ FILE: js/components/listener.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import Tone from 'tone'; AFRAME.registerComponent('listener', { init() {}, remove() {}, tick() { // const pos = this.el.object3D.position const object3d = this.el.object3D; object3d.updateMatrixWorld(); const matrixWorld = object3d.matrixWorld; const position = new THREE.Vector3().setFromMatrixPosition(matrixWorld); Tone.Listener.setPosition(position.x, position.y, position.z); const mOrientation = matrixWorld.clone(); mOrientation.setPosition({ x: 0, y: 0, z: 0 }); const vFront = new THREE.Vector3(0, 0, 1); vFront.applyMatrix4(mOrientation); vFront.normalize(); const vUp = new THREE.Vector3(0, -1, 0); vUp.applyMatrix4(mOrientation); vUp.normalize(); Tone.Listener.setOrientation(vFront.x, vFront.y, vFront.z, vUp.x, vUp.y, vUp.z); } }); ================================================ FILE: js/components/palette.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { Instruments, NoteCount, InstrumentCount, TotalNotes} from '../core/instruments'; import { BallColors, ControllerColors } from '../core/colors'; import { Shapes } from '../core/shapes'; const SPHERE_BASE_URL = './static/img/ball_'; const SPHERE_BASE_EXT = '.png'; AFRAME.registerComponent('palette', { schema: { type: 'int', default: 0 }, update() { // Dispose of the old instrument if (this._currentInstrument) { this._currentInstrument.dispose(); } // Set the new instrument this._currentInstrument = Instruments[this.data % Instruments.length]; }, trigger(time, tone, velocity, x, y, z) { this._currentInstrument.trigger(time, tone, velocity, x, y, z); }, totalNotes() { return TotalNotes; }, noteCount() { return NoteCount; }, colorPalette() { return BallColors[ this._currentInstrument.color ]; }, controllerColors() { return ControllerColors[ this._currentInstrument.color ]; }, shapePalette(id) { return Shapes[ id ]; }, loadSphereTextures() { let image134Circles = document.getElementById( 'ball_circles_134' ); let image567Circles = document.getElementById( 'ball_circles_567' ); let image134Triangles = document.getElementById( 'ball_triangles_134' ); let image567Triangles = document.getElementById( 'ball_triangles_567' ); let image134Squares = document.getElementById( 'ball_squares_134' ); let image567Squares = document.getElementById( 'ball_squares_567' ); this._textureSprite134Circles = new THREE.Texture( image134Circles ); this._textureSprite567Circles = new THREE.Texture( image567Circles ); this._textureSprite134Triangles = new THREE.Texture( image134Triangles ); this._textureSprite567Triangles = new THREE.Texture( image567Triangles ); this._textureSprite134Squares = new THREE.Texture( image134Squares ); this._textureSprite567Squares = new THREE.Texture( image567Squares ); this._textureSprite134Circles.wrapS = this._textureSprite134Circles.wrapT = THREE.RepeatWrapping; this._textureSprite567Circles.wrapS = this._textureSprite567Circles.wrapT = THREE.RepeatWrapping; this._textureSprite134Triangles.wrapS = this._textureSprite134Triangles.wrapT = THREE.RepeatWrapping; this._textureSprite567Triangles.wrapS = this._textureSprite567Triangles.wrapT = THREE.RepeatWrapping; this._textureSprite134Squares.wrapS = this._textureSprite134Squares.wrapT = THREE.RepeatWrapping; this._textureSprite567Squares.wrapS = this._textureSprite567Squares.wrapT = THREE.RepeatWrapping; setTimeout( () => { document.dispatchEvent(new Event("SPHERE_TEXTURE_LOADED")); },0); }, textureSprite134(id) { return [this._textureSprite134Circles, this._textureSprite134Squares, this._textureSprite134Triangles][id]; }, textureSprite567(id) { return [this._textureSprite567Circles, this._textureSprite567Squares, this._textureSprite567Triangles][id]; }, }); ================================================ FILE: js/components/proximity-check.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const getComponentProperty = AFRAME.utils.entity.getComponentProperty; const setComponentProperty = AFRAME.utils.entity.setComponentProperty; import { getParameterByName } from '../util'; AFRAME.registerComponent('proximity-check', { schema: { objects: { default: [], type: 'array' }, }, init: function() { }, play: function() { document.addEventListener('PLAYER_ADDED', (event) => { // adds to list this.data.objects.push(event.detail.entity); // when remote heads move check distances let head = document.querySelector("#head_"+event.detail.entity.id); let person = head.parentNode; person.addEventListener('componentchanged', (event) => { this.checkDistances(); }); this.checkDistances(); }); if(getParameterByName("proximity")!=="false"){ // when player moves check distances let player = document.querySelector("#player"); player.addEventListener('componentchanged', (event) => { this.checkDistances(); }); this.checkDistances(); } }, pause: function() { }, checkDistances:function() { let myPosition = this.el.object3D.getWorldPosition(); let entity; for(let i=0; i 0){ let ray = right.childNodes[0]; ray.setAttribute("opacity",opacity); let cone = right.childNodes[0].childNodes[0]; cone.setAttribute("opacity",opacity); } } } }, }); ================================================ FILE: js/components/quaternion.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * Quaternion. * * Represents orientation of object in three dimensions. Similar to `rotation` * component, but avoids problems of gimbal lock. * * See: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation */ AFRAME.registerComponent('quaternion', { schema: {type: 'vec4'}, update: function () { var data = this.data; this.el.object3D.quaternion.set(data.x, data.y, data.z, data.w); } }); ================================================ FILE: js/components/smooth-motion.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. var degToRad = require('three').Math.degToRad; function LowpassFilter(Fc) { var Q = 0.707; var K = Math.tan(Math.PI * Fc); var norm = 1 / (1 + K / Q + K * K); this.a0 = K * K * norm; this.a1 = 2 * this.a0; this.a2 = this.a0; this.b1 = 2 * (K * K - 1) * norm; this.b2 = (1 - K / Q + K * K) * norm; this.z1 = this.z2 = 0; this.value = 0; this.tick = function(value) { var out = value * this.a0 + this.z1; this.z1 = value * this.a1 + this.z2 - this.b1 * out; this.z2 = value * this.a2 - this.b2 * out; return out; }; } /** * Interpolate component for A-Frame. */ AFRAME.registerComponent('smooth-motion', { schema: { amount: { default: 1 } }, /** * Called once when component is attached. Generally for initial setup. */ init: function() { this.quaternion = new THREE.Quaternion(); }, /** * Called when component is attached and when component data changes. * Generally modifies the entity based on the data. */ update: function(oldData) { if (!this.filterPosX) { var amount = parseFloat(this.data.amount); if (amount > 0) { var freq = 0.1 / amount; this.filterPosX = new LowpassFilter(freq, this); this.filterPosY = new LowpassFilter(freq, this); this.filterPosZ = new LowpassFilter(freq, this); } } }, /** * Called when a component is removed (e.g., via removeAttribute). * Generally undoes all modifications to the entity. */ remove: function() {}, /** * Called on each scene tick. */ tick: function(t) { if (!this.filterPosX) { return; } var p = AFRAME.utils.entity.getComponentProperty(this.el, 'position'); this.el.object3D.position.setX(this.filterPosX.tick(p.x)); this.el.object3D.position.setY(this.filterPosY.tick(p.y)); this.el.object3D.position.setZ(this.filterPosZ.tick(p.z)); var quaternion; if (AFRAME.utils.entity.getComponentProperty(this.el, 'quaternion')) { var q = AFRAME.utils.entity.getComponentProperty(this.el, 'quaternion'); quaternion = new THREE.Quaternion(q._x, q._y, q._z, q._w); } else { var r = AFRAME.utils.entity.getComponentProperty(this.el, 'rotation'); quaternion = new THREE.Quaternion(); quaternion.setFromEuler(new THREE.Euler(degToRad(r.x), degToRad(r.y), degToRad(r.z), 'YXZ')); } this.quaternion.slerp(quaternion, 1 / (3 * parseFloat(this.data.amount))); this.el.object3D.quaternion.copy(this.quaternion); } }); ================================================ FILE: js/components/teleport.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const getComponentProperty = AFRAME.utils.entity.getComponentProperty; const setComponentProperty = AFRAME.utils.entity.setComponentProperty; import { getParameterByName } from '../util' import TWEEN from 'tween.js'; AFRAME.registerComponent('teleport', { isHitByRayCast: false, isAnimating: false, isOverSphere: false, floorTarget: null, doesControllerExist: false, schema: { }, init: function() { this.floorTarget = document.querySelector('#gaze-hit'); this.floorTarget.setAttribute("visible", "false"); }, play: function() { let onUp = (event) => { // flag passed from clicker js if (event.wasSphereHit) { return; } if(this.isHitByRayCast){ this.teleport(); } }; document.addEventListener('INTRO_COMPLETED', (event) => { setTimeout(() => { document.addEventListener("CONTROLLER_CREATED", (event) => { this.doesControllerExist = true; this.hideTeleport(); // disables teleport on click screen tap if (event.detail.id === "Daydream Controller") { document.removeEventListener('touchend', onUp); } // enables teleport on controller document.addEventListener('trackpadup', onUp); document.addEventListener('thumbstickup', onUp); }, false); let avatar = document.querySelector('#avatar'); if(getParameterByName("teleport")==="false"){ this.hideTeleport(); return; } document.addEventListener("touchstart", (event) => {}); document.addEventListener("touchend", (event) => {}); let isTablet = this.isTabletLikeDimensions() && this.isTouchDevice(); if (AFRAME.utils.device.isMobile() || isTablet) { document.addEventListener('mouseup', onUp); } else { this.hideTeleport(); return; } this.el.addEventListener('raycaster-intersected', (event) => { if(this.isOverSphere) { return; } if(this.isAnimating) { return; } if(this.isHitByRayCast) { return; } this.showTeleport(); }); this.el.addEventListener('raycaster-intersected-cleared', (event) => { if(this.isOverSphere) { return; } if(this.isAnimating) { return; } if(!this.isHitByRayCast) { return; } this.hideTeleport(); }); document.addEventListener('keyup', (event) => { switch (event.keyCode) { case 32: // spacebar this.onUp(event); break; } }); document.addEventListener('ON_SPHERE_IN', (event) => { this.isOverSphere = true; this.hideTeleport(); }); document.addEventListener('ON_SPHERE_OUT', (event) => { this.isOverSphere = false; }); }); }); }, hideTeleport: function() { this.floorTarget.setAttribute("visible", "false"); this.isHitByRayCast = false; }, showTeleport: function() { this.floorTarget.setAttribute("visible", "true"); this.isHitByRayCast = true; }, teleport: function() { if (!this.getTreeCreated()) { return; } if (this.isNearSphere()) { return; } if (!this.isHitByRayCast) { return; } this.isAnimating = true; const target = document.querySelector('#gaze-hit'); const speed = 100; const player = document.querySelector("#player"); const avatar = document.querySelector('#avatar'); const daydream = document.querySelector('#daydream'); let targetPosition = target.object3D.position; let position = getComponentProperty(player,"position"); let moveTo = (data) => { setComponentProperty(player, "position", position); if (!daydream) { return; } setComponentProperty(daydream, "position", position); }; if (this.flyTween) { this.flyTween.stop(); } this.flyTween = new TWEEN.Tween(position) .to(targetPosition.clone(), speed) .easing(TWEEN.Easing.Exponential.InOut) .onUpdate(moveTo) .onComplete(()=>{ this.isAnimating = false; this.hideTeleport(); }) .start(); }, pause: function() { }, tick: function() { if(this.isAnimating) { return; } let cam = document.querySelector("[raycaster]").object3D; let orientation = cam.getWorldDirection(); orientation.y *= -1; orientation.z *= -1; orientation.x *= -1; let ray = new THREE.Ray(cam.getWorldPosition(), orientation); let point = ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0,1,0))); if(point) { point.y = 0.02; this.floorTarget.setAttribute('position', point); } }, getTreeCreated: function() { return this.el.sceneEl.systems.tree.getTreeCreated(); }, isNearSphere: function() { let nearSphereCount = 0; let elements = document.querySelectorAll('[grab-move]'); let entity; for(let i=0; i h) ? w : h) >= 960); }, isTouchDevice: function() { return 'ontouchstart' in window || navigator.maxTouchPoints; // works on most browsers || works on IE10/11 and Surface }, }); ================================================ FILE: js/components/tone.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import Tone from 'tone'; import {scale} from '../util'; import { Instruments, NoteCount, InstrumentCount, TotalNotes} from '../core/instruments'; //create the background track const filetype = Tone.Buffer.supportsType('mp3') ? 'mp3' : 'ogg'; const ambient = new Tone.Player(`static/audio/bg.${filetype}`, () => { ambient.start(); ambient.volume.rampTo(-14, 4); }).toMaster(); ambient.volume.value = -Infinity; ambient.loop = true; window.addEventListener('blur', () => { Tone.Master.mute = true; }); window.addEventListener('focus', () => { Tone.Master.mute = false; }); document.addEventListener("ENTERED_FOREST", () => { Tone.Master.mute = false; }); document.addEventListener("EXITED_FOREST", () => { Tone.Master.mute = true; }); AFRAME.registerComponent('tone', { schema: { type: 'int', default: 0 }, init() { // listen for events this.el.addEventListener('controllerhit', this.controllerHit.bind(this)); this._palette = this.el.sceneEl.components.palette; this._lastTrigger = 0; }, remove() { }, update(oldData) { if (this.data !== oldData && typeof oldData !== "undefined") { this.trigger(Tone.now() + 0.1, 0.4); } }, getPosition() { const object3d = this.el.object3D; object3d.updateMatrixWorld(); const matrixWorld = object3d.matrixWorld; const position = new THREE.Vector3().setFromMatrixPosition(matrixWorld); return position; }, trigger(time = Tone.now(), velocity = 1) { // this.env.triggerAttack(time, velocity) const pos = this.getPosition(); Instruments[this.getShape()].trigger(time, this.getNote(), velocity, pos.x, pos.y, pos.z); }, hit(time, velocity, controllerPosition = null, sourceId = null) { // hits.push('#' + this.el.id); this.trigger(time, velocity); this.el.emit('hit', { from: sourceId, velocity: velocity, delay: time - Tone.now(), controllerPosition: controllerPosition }); }, updatePosition() { const object3d = this.el.object3D; object3d.updateMatrixWorld(); const matrixWorld = object3d.matrixWorld; const position = new THREE.Vector3().setFromMatrixPosition(matrixWorld); this.panner.setPosition(position.x, position.y, position.z); }, controllerHit(data) { if( !this.el.getAttribute("visible")) {return;} const time = Tone.Time('+0.01').toSeconds(); if ((time - this._lastTrigger) > 0.1) { this._lastTrigger = time; this.hit(time, data.detail.velocity, data.detail.controllerPosition); } }, //return the shape number of the tone 0-2 getShape(){ return Math.floor(this.data / NoteCount); }, //return the note number of the tone 0-5 getNote(){ return this.data % NoteCount; }, //return the total number of tones //all note/shape getTotalTones(){ return TotalNotes; }, }); ================================================ FILE: js/components/tool-tips.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const getComponentProperty = AFRAME.utils.entity.getComponentProperty; const setComponentProperty = AFRAME.utils.entity.setComponentProperty; AFRAME.registerComponent('tool-tips', { tooltip: null, sprite: null, clock: null, tilesIncX : 6, tilesIncY : 8, totalTiles : 48, tileTime: 48*4, currentTime:0, currentTile:0, schema: { hand: { default: "right", type: "string" }, timeout: { default: 30000, type: "int" } }, init: function() { this.clock = new THREE.Clock(); }, play: function() { // adds timeout for tool tip removal let addToolTipTimeout = () => { document.removeEventListener("TREE_CREATED", addToolTipTimeout); setTimeout(() => { removeToolTipsBind(); }, this.data.timeout); }; let removeToolTipsBind = () => { document.removeEventListener("TREE_CREATED", addToolTipTimeout); this.el.removeEventListener('triggerup', removeToolTipsBind); this.removeToolTips(); }; let addToolTips = () => { let loader = new THREE.TextureLoader(); loader.load("./static/img/toot_tip_"+this.data.hand+".png", (texture) => { this.sprite = texture; this.sprite.wrapS = THREE.RepeatWrapping; this.sprite.wrapT = THREE.RepeatWrapping; this.sprite.repeat.set( 1/this.tilesIncX, 1/this.tilesIncY ); let material = new THREE.MeshBasicMaterial({ map: this.sprite, side: THREE.DoubleSide }); let geometry = new THREE.PlaneGeometry( 0.1, 0.05, 1 ); this.tooltip = new THREE.Mesh(geometry, material); this.tooltip.rotation.x = THREE.Math.degToRad(-90); this.tooltip.position.x = (this.data.hand==="right") ? -0.085 : 0.085; this.tooltip.position.z = 0.075; this.el.object3D.add(this.tooltip); }); // timeout required due to triggerup firing prematurely setTimeout(() => { this.el.addEventListener('triggerup', removeToolTipsBind); if(this.isTreeCreated()){ addToolTipTimeout(); } else { document.addEventListener("TREE_CREATED", addToolTipTimeout); } }); }; // adds tooltips once object 3d is ready let checkForToolTips = () => { if(this.el.object3D) { addToolTips(); return; } requestAnimationFrame(checkForToolTips); }; checkForToolTips(); }, removeToolTips: function() { if(!this.tooltip) { return; } this.el.object3D.remove(this.tooltip); this.el.removeAttribute("tool-tips"); }, pause: function() { }, tick: function() { if(!this.tooltip) { return; } this.currentTime += (1000 * this.clock.getDelta()); while (this.currentTime > this.tileTime) { this.currentTime -= this.tileTime; this.currentTile++; if (this.currentTile == this.totalTiles){ this.currentTile = 0; } this.sprite.offset.x = Math.floor( this.currentTile / this.tilesIncX ) / this.tilesIncX; this.sprite.offset.y = ( this.currentTile % this.tilesIncX ) / this.tilesIncY; } }, isTreeCreated: function() { return this.el.sceneEl.systems.tree.isTreeCreated; } }); ================================================ FILE: js/components/touch-color.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { getViewerType, getParameterByName } from '../util' import { ControllerColors } from '../core/colors'; import TWEEN from 'tween.js'; const getComponentProperty = AFRAME.utils.entity.getComponentProperty; const setComponentProperty = AFRAME.utils.entity.setComponentProperty; AFRAME.registerComponent('touch-color', { dependencies: ['tracked-controls', 'brush'], tickTween: null, trackPosStart: null, trackPos: null, prevTrackPos: null, disc: null, rotationChange: 0, totalTicks: 20, ticks: null, lastTone : 4, schema: { isClockwise: { default: true }, isTracking: { default: false }, trackPos: { default: new THREE.Vector2() }, isTrackPadTouched: { default: false }, degrees: { default: 0 }, color: { type: 'string', default: 'red' }, }, init: function() { this.prevTrackPos = new THREE.Vector2(1, 1); setTimeout(() => { let material, geometry, ball; ball = this.el.object3D; if (this.el.id === "avatar") { } else { geometry = new THREE.CylinderGeometry(0.02, 0.02, 0.01, 32); material = new THREE.MeshBasicMaterial({ color: this.getColor() }); this.disc = new THREE.Mesh(geometry, material); this.disc.name = "disc"; // this.disc.rotation.x = THREE.Math.degToRad( 6 ); this.disc.position.x = 0; this.disc.position.y = 0.001; this.disc.position.z = 0.0728 - 0.025; let tick, theta; this.ticks = []; for (let i = 0; i < this.totalTicks; i++) { geometry = new THREE.CylinderGeometry(0.001, 0.001, 0.011, 12); material = new THREE.MeshBasicMaterial({ color: 0xFFFFFF }); tick = new THREE.Mesh(geometry, material); theta = i / this.totalTicks * Math.PI * 2; tick.position.x = Math.cos(theta) * 0.017; tick.position.z = Math.sin(theta) * 0.017; this.ticks.push(tick); this.disc.add(tick); } ball.add(this.disc); } }, 1); this.el.addEventListener("componentchanged", function(e) { if (e.detail.name === "grab-move") { //if the attribute changed if (e.detail.oldData.grabbing !== e.detail.newData.grabbing) { // reset the rotation detail this.rotationChange = 0; } } }); document.addEventListener("SPHERE_TEXTURE_LOADED", (event) => { this.update(); }); }, play: function() { let el = this.el; if (this.el.id === "avatar" && getParameterByName('reticle')==='true') { document.addEventListener('keyup', this.onKeyUp.bind(this)); document.addEventListener('keydown', this.onKeyDown.bind(this)); } el.addEventListener('trackpaddown', this.onTrackPadTouchStart.bind(this)); el.addEventListener('trackpadup', this.onTrackPadTouchEnd.bind(this)); el.addEventListener('touchstart', this.onTrackPadTouchStart.bind(this)); el.addEventListener('touchend', this.onTrackPadTouchEnd.bind(this)); el.addEventListener('axismove', this.onTrackPadMove.bind(this)); }, pause: function() { let el = this.el; if (this.el.id === "avatar" && getParameterByName('reticle')==='true') { document.removeEventListener('keyup', this.onKeyUp); document.removeEventListener('keydown', this.onKeyDown); } el.removeEventListener('trackpaddown', this.onTrackPadTouchStart); el.removeEventListener('trackpadup', this.onTrackPadTouchEnd); el.removeEventListener('touchstart', this.onTrackPadTouchStart); el.removeEventListener('touchend', this.onTrackPadTouchEnd); el.removeEventListener('axismove', this.onTrackPadMove); }, // 57, 48 onKeyUp: function(event) { switch (event.keyCode) { // close bracket case 32: // document.dispatchEvent(new Event("TRACKPAD_UP")); break; } }, onKeyDown: function(event) { switch (event.keyCode) { // < case 188: this.selectSound(-0.03); break; // > case 190: // this.rotateSound(1); this.selectSound(0.03); break; } }, selectSound: function(increment) { let grabComponent = getComponentProperty(this.el, "grab-move"); if (!grabComponent.isNearSphere) { return; } this.rotationChange += increment; const incrementAmount = 0.02; if (Math.abs(this.rotationChange) > incrementAmount) { this.rotationChange = this.rotationChange % incrementAmount; grabComponent.closestSphere.components.ball.note.setRotationIncrement(this.rotationChange > 0); // move the tone value let totalNotes = this.el.sceneEl.components.palette.totalNotes(); let tone = grabComponent.closestSphere.getAttribute("tone"); tone = this.rotationChange > 0 ? tone + 1 : tone - 1; tone = tone % (totalNotes); tone = (tone<0) ? (totalNotes - 1) : tone; this.lastTone = tone; grabComponent.closestSphere.setAttribute("tone", tone); } }, onTrackPadTouchStart: function(event) { this.trackPosStart = null; this.data.isTrackPadTouched = true; }, onTrackPadTouchEnd: function(event) { this.trackPosStart = null; this.data.isTrackPadTouched = false; }, onTrackPadMove: function(event) { // HACK - trackpad move is firing on trigger.x = 0 fixes this if (this.data.isTrackPadTouched === false || event.detail.axis[0] === 0) { return; } this.data.trackPos.fromArray(event.detail.axis, 0); if (this.el.hasAttribute("vive-controls")) { this.data.trackPos.y *= -1; } this.data.trackPos.normalize(); this.data.isTracking = (this.data.trackPos.distanceToSquared(this.prevTrackPos) > 0.001); if (!this.trackPosStart) { this.trackPosStart = this.data.trackPos.clone(); } let radiansA = Math.atan2(this.data.trackPos.y, this.data.trackPos.x); let radiansB = Math.atan2(this.trackPosStart.y, this.data.trackPos.x); this.data.degrees = Math.abs((radiansA - radiansB) * 180 / Math.PI); let cross = (this.prevTrackPos.x * this.data.trackPos.y) - (this.prevTrackPos.y * this.data.trackPos.x); if (cross !== 0) { this.data.isClockwise = (cross > 0); } this.prevTrackPos.copy(this.data.trackPos); if (!this.data.isTracking || !this.data.isTrackPadTouched ) { return; } this.selectSound(0.003 * (this.data.isClockwise ? 1 : -1)); this.data.trackPos.multiplyScalar(0.1); if (this.data.trackPos.lengthSq() > 0.005) { let angle = this.data.trackPos.angle() * (180 / Math.PI); let tickSeed = Math.floor((angle / 360) * this.totalTicks); let tick = this.ticks[tickSeed]; let scalar = { scale: 2 }; this.tickTween = new TWEEN.Tween({ scale: 0 }) .to({ scale: 1 }, 250) .onUpdate((percentage) => { tick.scale.x = 1 - percentage + 1; tick.scale.z = 1 - percentage + 1; }); this.tickTween.start(); } }, tick: function() { }, update: function( oldData ) { if ( !this.disc ) { return; } this.disc.material.color = this.getColor(); }, getColor: function() { return new THREE.Color( this.el.sceneEl.components.palette.controllerColors()[1] ); } }); function tweenUpdate() { requestAnimationFrame(tweenUpdate); TWEEN.update(); } tweenUpdate(); ================================================ FILE: js/components/tree.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { ShadowColor } from '../core/colors'; const getComponentProperty = AFRAME.utils.entity.getComponentProperty; const setComponentProperty = AFRAME.utils.entity.setComponentProperty; const stringify = AFRAME.utils.coordinates.stringify; AFRAME.registerSystem('tree', { tree: null, treeTimeOut:null, isTreeCreated:false, schema: { }, init: function() { document.addEventListener("TREE_CREATED", (event) => { setTimeout(() => { let balls = tree.getElementsByClassName("selectable"); }, 100); }); document.addEventListener("ON_TRIGGER_EMPTY_SPACE", (event) => { let newBallData = { id: "TEMP_NEW_SPHERE_ID", position: stringify(event.detail.controllerPos), hand: event.detail.hand, tone: event.detail.lastTone }; this.createSphere(newBallData); ga('send', 'event', "interaction", "create"); }, false); document.addEventListener("ON_DELETE", (event) => { this.deleteSphere(event.detail.closestEntity); ga('send', 'event', "interaction", "delete"); }, false); this.createTree(); }, createTree: function() { this.tree = document.createElement('a-entity'); this.tree.id = "tree"; setComponentProperty(this.tree, "position","0 0 0"); this.sceneEl.appendChild(this.tree); }, createSphere: function(ballData) { if (!ballData) throw new Error("No ball data"); let entity = document.createElement('a-entity'); entity.id = ballData.id; entity.className = "ball"; entity.className += " selectable"; this.tree.appendChild(entity); setComponentProperty(entity, "ball", {hand:ballData.hand}); setComponentProperty(entity, "tone", ballData.tone); setComponentProperty(entity, "position", ballData.position); setComponentProperty(entity, "visible", ballData.visible); setComponentProperty(entity, 'smooth-motion', 'amount:3'); setComponentProperty(entity, 'ga', true); // setComponentProperty(entity, "mixin", "obj-ball"); setComponentProperty(entity, "copresence", {uuid:ballData.uuid}); if(!this.isTreeCreated){ clearTimeout(this.treeTimeOut); this.treeTimeOut = setTimeout(() => { this.isTreeCreated = true; document.dispatchEvent(new Event("TREE_CREATED")); },0); } }, deleteSphere: function(ballData) { let entity = document.querySelector("#" + ballData.id); if (!entity) { return; } let component = getComponentProperty(entity, "ball"); // remove ball setTimeout(() => { this.tree.removeChild(entity); }, 0); }, truncateDecimals: function(number, digits) { let multiplier = Math.pow(10, digits), adjustedNum = number * multiplier, truncatedNum = Math[adjustedNum < 0 ? 'ceil' : 'floor'](adjustedNum); return truncatedNum / multiplier; }, switchRoom:function(){ this.sceneEl.systems["copresence-server"].switchRoom(); }, clear: function(){ let tree = document.querySelector("#tree"); let balls = tree.getElementsByClassName("selectable"); // delete balls on timer let setDeleteTimer = (ball, timer) => { setTimeout((event) => { this.deleteSphere(ball); },timer); }; for(let i=0; i { let radius = this.data.maxRadius; this.el.addEventListener("componentchanged", event => { if (event.detail.name === "position") { let position = event.detail.newData; if (position.z > radius) { position.z = radius; } if (position.z < -radius) { position.z = -radius; } if (position.x > radius) { position.x = radius; } if (position.x < -radius) { position.x = -radius; } setComponentProperty(this.el, "position", position); } }); }); }, play: function() { }, pause: function() { }, }); ================================================ FILE: js/core/color-set.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. export class ColorSet { constructor( background, idle, active, stem ) { this.background = new THREE.Color( background ); this.idle = new THREE.Color( idle ); this.active = new THREE.Color( active ); this.stem = new THREE.Color( stem ); } setUniforms( material ) { material.uniforms.bgColor.value = this.background; material.uniforms.idleColor.value = this.idle; material.uniforms.activeColor.value = this.active; } } ================================================ FILE: js/core/colors.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { ColorSet } from './color-set'; const BallColors = { blue: [ // bg idle active stem //spheres - yellow to orange new ColorSet( 0xFFC362, 0xFFA544, 0xFFE180, 0xFFA544 ), new ColorSet( 0xFFB564, 0xFF9746, 0xFFD382, 0xFF9746 ), new ColorSet( 0xFFA765, 0xFF8947, 0xFFC583, 0xFF8947 ), new ColorSet( 0xFF9A67, 0xFF7C49, 0xFFB885, 0xFF7C49 ), new ColorSet( 0xFF8C68, 0xFF6E4A, 0xFFAA86, 0xFF6E4A ), new ColorSet( 0xFF7E6A, 0xFF604C, 0xFF9C88, 0xFF604C ), //squares - blue to green new ColorSet( 0x49A6E5, 0x308DCC, 0x6CC9FF, 0x308DCC ), new ColorSet( 0x3AAED3, 0x2195BA, 0x5DD1F6, 0x2195BA ), new ColorSet( 0x2CB7C1, 0x139EA8, 0x4FDAE4, 0x048F99 ), new ColorSet( 0x1DBFAE, 0x04A695, 0x40E2D1, 0x04A695 ), new ColorSet( 0x0FC89C, 0x00AF83, 0x32EBBF, 0x00AF83 ), new ColorSet( 0x00D08A, 0x00B771, 0x23F3AD, 0x00B771 ), //triangles - purple to light blue new ColorSet( 0xAC44C8, 0x932AAF, 0xDE76FA, 0x982FB4 ), new ColorSet( 0xA258D3, 0x893FBA, 0xD48AFF, 0x8E44BF ), new ColorSet( 0x986DDE, 0x7F54C5, 0xCA9FFF, 0x8459CA ), new ColorSet( 0x8D81E8, 0x7468CF, 0xBFB3FF, 0x796DD4 ), new ColorSet( 0x8396F3, 0x6A7DDA, 0xB5C8FF, 0x6F82DF ), new ColorSet( 0x79AAFE, 0x6091E5, 0xABDCFF, 0x6596EA ), ], red: [ // bg idle active stem //spheres - yellow to orange new ColorSet( 0xFFC362, 0xFFA544, 0xFFE180, 0xFFA544 ), new ColorSet( 0xFFB564, 0xFF9746, 0xFFD382, 0xFF9746 ), new ColorSet( 0xFFA765, 0xFF8947, 0xFFC583, 0xFF8947 ), new ColorSet( 0xFF9A67, 0xFF7C49, 0xFFB885, 0xFF7C49 ), new ColorSet( 0xFF8C68, 0xFF6E4A, 0xFFAA86, 0xFF6E4A ), new ColorSet( 0xFF7E6A, 0xFF604C, 0xFF9C88, 0xFF604C ), //squares - blue to green new ColorSet( 0x49A6E5, 0x308DCC, 0x6CC9FF, 0x308DCC ), new ColorSet( 0x3AAED3, 0x2195BA, 0x5DD1F6, 0x2195BA ), new ColorSet( 0x2CB7C1, 0x139EA8, 0x4FDAE4, 0x048F99 ), new ColorSet( 0x1DBFAE, 0x04A695, 0x40E2D1, 0x04A695 ), new ColorSet( 0x0FC89C, 0x00AF83, 0x32EBBF, 0x00AF83 ), new ColorSet( 0x00D08A, 0x00B771, 0x23F3AD, 0x00B771 ), //triangles - purple to light blue new ColorSet( 0xAC44C8, 0x932AAF, 0xDE76FA, 0x982FB4 ), new ColorSet( 0xA258D3, 0x893FBA, 0xD48AFF, 0x8E44BF ), new ColorSet( 0x986DDE, 0x7F54C5, 0xCA9FFF, 0x8459CA ), new ColorSet( 0x8D81E8, 0x7468CF, 0xBFB3FF, 0x796DD4 ), new ColorSet( 0x8396F3, 0x6A7DDA, 0xB5C8FF, 0x6F82DF ), new ColorSet( 0x79AAFE, 0x6091E5, 0xABDCFF, 0x6596EA ), ], green: [ // bg idle active stem //spheres - yellow to orange new ColorSet( 0xFFC362, 0xFFA544, 0xFFE180, 0xFFA544 ), new ColorSet( 0xFFB564, 0xFF9746, 0xFFD382, 0xFF9746 ), new ColorSet( 0xFFA765, 0xFF8947, 0xFFC583, 0xFF8947 ), new ColorSet( 0xFF9A67, 0xFF7C49, 0xFFB885, 0xFF7C49 ), new ColorSet( 0xFF8C68, 0xFF6E4A, 0xFFAA86, 0xFF6E4A ), new ColorSet( 0xFF7E6A, 0xFF604C, 0xFF9C88, 0xFF604C ), //squares - blue to green new ColorSet( 0x49A6E5, 0x308DCC, 0x6CC9FF, 0x308DCC ), new ColorSet( 0x3AAED3, 0x2195BA, 0x5DD1F6, 0x2195BA ), new ColorSet( 0x2CB7C1, 0x139EA8, 0x4FDAE4, 0x048F99 ), new ColorSet( 0x1DBFAE, 0x04A695, 0x40E2D1, 0x04A695 ), new ColorSet( 0x0FC89C, 0x00AF83, 0x32EBBF, 0x00AF83 ), new ColorSet( 0x00D08A, 0x00B771, 0x23F3AD, 0x00B771 ), //triangles - purple to light blue new ColorSet( 0xAC44C8, 0x932AAF, 0xDE76FA, 0x982FB4 ), new ColorSet( 0xA258D3, 0x893FBA, 0xD48AFF, 0x8E44BF ), new ColorSet( 0x986DDE, 0x7F54C5, 0xCA9FFF, 0x8459CA ), new ColorSet( 0x8D81E8, 0x7468CF, 0xBFB3FF, 0x796DD4 ), new ColorSet( 0x8396F3, 0x6A7DDA, 0xB5C8FF, 0x6F82DF ), new ColorSet( 0x79AAFE, 0x6091E5, 0xABDCFF, 0x6596EA ), ], }; const ControllerColors = { // bg grain blue: [ 0x5396f2, 0x5261f2 ], red: [ 0xec4c4e, 0xc14040 ], green: [ 0x93cbc4, 0x58a868 ] }; const BgTreeColors = [ 0x827193, 0xa08293, 0xc59498 ]; const EnvColors = { floor: 0xF79F99, fog: 0xe9a69a }; const ShadowColor = 0xdf978b; const HeadsetColor = 0xE8E8E8; const HeadsetShadow = 0xAFAFAF; export { BallColors, ControllerColors, BgTreeColors, EnvColors, ShadowColor, HeadsetColor, HeadsetShadow }; ================================================ FILE: js/core/instrument.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import Tone from 'tone'; export class Instrument { constructor( data ) { const folder = data.name; const urls = {}; const filetype = Tone.Buffer.supportsType('mp3') ? 'mp3' : 'ogg'; for (let i = 0; i < data.noteCount; i++){ urls[i] = `static/audio/${folder}/${i}.${filetype}`; } this._buffers = new Tone.Buffers(urls); this.color = data.color; this.shape = data.shape; this.output = new Tone.Gain().toMaster(); this.output.gain.value = 2; } trigger(time, tone, velocity, x, y, z){ if (this._buffers.loaded){ const source = this._createSource(time, tone, velocity); const panner = this._createPanner(x, y, z); source.connect(panner); } } _createPanner(x, y, z){ const panner = Tone.context.createPanner(); panner.rolloffFactor = 2; panner.setPosition(x, y, z); panner.connect(this.output); return panner; } _createSource(time, note, velocity){ const buffer = this._buffers.get(note); const source = new Tone.BufferSource(buffer); source.start(time, 0, undefined, velocity * 0.5, 0.01); return source; } dispose(){ this.output.dispose(); } } ================================================ FILE: js/core/instruments.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { Instrument } from './instrument'; export const NoteCount = 6; export const Instruments = [ new Instrument({ name: 'percussion', color: 'blue', shape: 'circles', noteCount : NoteCount }), new Instrument({ name: 'marimba', color: 'red', shape: 'squares', noteCount : NoteCount }), new Instrument({ name: 'voice', color: 'green', shape: 'triangles', noteCount : NoteCount }), ]; export const InstrumentCount = Instruments.length; export const TotalNotes = NoteCount * InstrumentCount; ================================================ FILE: js/core/shape-data.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const TONE_CHANNEL_MAP = [ new THREE.Vector3( 1, 0, 0 ), new THREE.Vector3( 1, 0, 0 ), new THREE.Vector3( 0, 1, 0 ), new THREE.Vector3( 0, 0, 1 ), new THREE.Vector3( 1, 0, 0 ), new THREE.Vector3( 0, 1, 0 ), new THREE.Vector3( 0, 0, 1 ) ]; export class ShapeData { constructor( layout, repeats, tone ) { this.layout = layout; this.repeats = repeats; this.tone = tone; } setUniforms( material ) { material.uniforms.spriteLayout.value = this.layout; material.uniforms.spriteRepeat.value = this.repeats; material.uniforms.spriteChannel.value = TONE_CHANNEL_MAP[ this.tone ]; material.uniforms.use567.value = this.tone > 3 ? 1.0 : 0.0; } } ================================================ FILE: js/core/shapes.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { ShapeData } from './shape-data'; export const Shapes = { 'circles': [ new ShapeData( new THREE.Vector2( 8, 4 ), new THREE.Vector2( 4, 1 ), 0 ), new ShapeData( new THREE.Vector2( 8, 4 ), new THREE.Vector2( 4, 1 ), 1 ), new ShapeData( new THREE.Vector2( 8, 4 ), new THREE.Vector2( 4, 1 ), 2 ), new ShapeData( new THREE.Vector2( 8, 4 ), new THREE.Vector2( 3, 1 ), 3 ), new ShapeData( new THREE.Vector2( 8, 4 ), new THREE.Vector2( 3, 1 ), 4 ), new ShapeData( new THREE.Vector2( 6, 6 ), new THREE.Vector2( 2, 1 ), 5 ), new ShapeData( new THREE.Vector2( 6, 6 ), new THREE.Vector2( 2, 1 ), 6 ) ], 'triangles': [ new ShapeData( new THREE.Vector2( 9, 4 ), new THREE.Vector2( 4, 1 ), 0 ), new ShapeData( new THREE.Vector2( 9, 4 ), new THREE.Vector2( 4, 1 ), 1 ), new ShapeData( new THREE.Vector2( 9, 4 ), new THREE.Vector2( 4, 1 ), 2 ), new ShapeData( new THREE.Vector2( 6, 6 ), new THREE.Vector2( 3, 1 ), 3 ), new ShapeData( new THREE.Vector2( 7, 5 ), new THREE.Vector2( 3, 1 ), 4 ), new ShapeData( new THREE.Vector2( 7, 5 ), new THREE.Vector2( 3, 1 ), 5 ), new ShapeData( new THREE.Vector2( 6, 6 ), new THREE.Vector2( 3, 1 ), 6 ) ], 'squares': [ new ShapeData( new THREE.Vector2( 2, 17 ), new THREE.Vector2( 1, 4 ), 0 ), new ShapeData( new THREE.Vector2( 2, 17 ), new THREE.Vector2( 1, 4 ), 1 ), new ShapeData( new THREE.Vector2( 3, 11 ), new THREE.Vector2( 1, 3 ), 2 ), new ShapeData( new THREE.Vector2( 3, 11 ), new THREE.Vector2( 1, 3 ), 3 ), new ShapeData( new THREE.Vector2( 3, 11 ), new THREE.Vector2( 1, 3 ), 4 ), new ShapeData( new THREE.Vector2( 3, 11 ), new THREE.Vector2( 1, 2 ), 5 ), new ShapeData( new THREE.Vector2( 3, 11 ), new THREE.Vector2( 1, 2 ), 6 ) ] }; ================================================ FILE: js/index.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // debug require('./util/trace.js'); require('./util/browserCheck.js'); // core require('aframe'); require('./ascene.js'); require('webvr-ui'); require('./components/copresence-server.js'); require('./components/headset-material.js'); require('./components/bg-tree-ring-material.js'); require('../third_party/aframe-daydream-controller-component/daydream-controller.js'); require('./components/daydream-manager.js'); require('./components/daydream-pointer.js'); require('./components/fake-light.js'); require('./components/tool-tips.js'); require('./components/controller-material.js'); // utility require('./components/quaternion.js'); require('./components/smooth-motion.js'); require('./components/controllers.js'); require('./components/haptics.js'); require('./components/ga.js'); // shaders require( './shaders/bg-tree-shader' ); require( './shaders/circle-shader' ); require( './shaders/ball-shader' ); // tree require('./components/tree.js'); require('./components/ball.js'); require('./components/background-objects.js'); // interaction require('./components/gaze.js'); require('./components/grab-move.js'); require('./components/touch-color.js'); require('./components/teleport.js'); require('./components/proximity-check.js'); require('./components/clicker.js'); require('./components/wasd-boundaries.js'); //sound require('./components/tone.js'); require('./components/palette.js'); require('./components/listener.js'); //the splash screen require('./splash'); ================================================ FILE: js/notes/note-head-cube.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { NoteHead } from './note-head'; import { RandomRange3D } from '../util/random-range-3d'; const CUBE_GEOMETRY = new THREE.BoxGeometry( 0.1, 0.1, 0.1 ); const RANDOM_ROTATION = new RandomRange3D( new THREE.Euler( -3.14, -3.14, -3.14 ), new THREE.Euler( +3.14, +3.14, +3.14 ) ); export class NoteHeadCube extends NoteHead { get geometry() { return CUBE_GEOMETRY; } get rotation() { return RANDOM_ROTATION.value; } } ================================================ FILE: js/notes/note-head-sphere.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { NoteHead } from './note-head'; import { RandomRange3D } from '../util/random-range-3d'; const CUBE_GEOMETRY = new THREE.IcosahedronGeometry( 0.05, 2 ); const RANDOM_ROTATION = new RandomRange3D( new THREE.Euler( -3.14, -3.14, -3.14 ), new THREE.Euler( +3.14, +3.14, +3.14 ) ); export class NoteHeadSphere extends NoteHead { get geometry() { return CUBE_GEOMETRY; } get rotation() { return RANDOM_ROTATION.value; } } ================================================ FILE: js/notes/note-head-tetra.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { NoteHead } from './note-head'; import { RandomRange3D } from '../util/random-range-3d'; import { Tetrahedron } from '../util/tetrahedron'; const TETRA_GEOMETRY = new Tetrahedron().geometry; const RANDOM_ROTATION = new RandomRange3D( new THREE.Euler( -0.3, -0.3, -0.3 ), new THREE.Euler( +0.3, +0.3, +0.3 ) ); export class NoteHeadTetra extends NoteHead { constructor( palette, shape ) { super( palette, shape ); // Enable flat shading this.material.extensions = { derivatives: true }; this.material.defines = { FLAT_SHADED: true }; } get geometry() { return TETRA_GEOMETRY; } get rotation() { return RANDOM_ROTATION.value; } } ================================================ FILE: js/notes/note-head.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const stringify = AFRAME.utils.coordinates.stringify; import TWEEN from 'tween.js'; export class NoteHead { constructor( palette, shape ) { this.palette = palette; this.shape = shape; this.shader = THREE.BallShader; this.uniforms = THREE.UniformsUtils.clone( this.shader.uniforms ); this.material = new THREE.ShaderMaterial({ uniforms: this.uniforms, vertexShader: this.shader.vertexShader, fragmentShader: this.shader.fragmentShader }); this.mesh = new THREE.Mesh( this.geometry, this.material ); } setTone( value ) { this.tone = value; this.scale = ( 1 - this.tone / ( this.palette.totalNotes() - 1 ) ) * 2 + 1; this.mesh.scale.set( this.scale, this.scale, this.scale ); setTimeout( () => { let textureId = Math.floor( this.tone / this.palette.noteCount() ); let textureOrder = this.tone % this.palette.noteCount(); let textureData = ["circles","squares","triangles"][textureId]; this.palette.shapePalette( textureData )[ textureOrder ].setUniforms( this.material ); this.palette.colorPalette()[ this.tone ].setUniforms( this.material ); let texture134 = this.palette.textureSprite134( textureId ); let texture567 = this.palette.textureSprite567( textureId ); texture134.needsUpdate = true; texture567.needsUpdate = true; this.material.uniforms.map134.value = texture134; this.material.uniforms.map567.value = texture567; this.material.uniforms.lightPosition.value.copy( this.lightPosition ); },0); } highlight( event ) { this.material.uniforms.brightnessAmount.value = 0.2; } unHighlight( event ) { this.material.uniforms.brightnessAmount.value = 0.0; } hit( event, timeMs ) { // Cancel tween if it already exists if ( this.hitTween ) { this.hitTween.stop(); } this.hitPosition = event.detail.controllerPosition; this.offsetPosition = this.mesh.getWorldPosition(); if(this.hitPosition) { this.offsetPosition.sub(this.hitPosition); } this.hitVelocity = (event.detail.velocity===0) ? 1 : event.detail.velocity; this.hitTween = new TWEEN.Tween( { t: this.hitVelocity } ) .to( { t: 0 }, timeMs ) .easing( TWEEN.Easing.Elastic.Out ) .delay( event.detail.delay * 1000 ) .onUpdate( (t) => { this.updateHitTween( t ); }) .onComplete( () => { this.updateHitTween( 1 ); }); this.hitTween.start(); this.hitFrameTween = new TWEEN.Tween( { t: 0 } ) .to( { t: 1 }, timeMs*1.25 ) .easing( TWEEN.Easing.Linear.None ) .delay( event.detail.delay * 1000 ) .onUpdate( (t) => { this.material.uniforms.spriteIndex.value = Math.floor( t * 32 ); }) .onComplete( () => { this.material.uniforms.spriteIndex.value = Math.floor( 1 * 32 ); }); this.hitFrameTween.start(); } updateHitTween( t ) { let s = ( 1 - t ) * this.hitVelocity + this.scale; this.mesh.scale.set( s, s, s ); this.material.uniforms.activeAmount.value = 1 - t; if(this.hitPosition) { let p = ( 1 - t ); this.mesh.position.x = this.offsetPosition.x*p*this.hitVelocity; this.mesh.position.y = this.offsetPosition.y*p*this.hitVelocity; } } get geometry() { } get rotation() { } } ================================================ FILE: js/notes/note-shadow-cube.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { NoteShadow } from './note-shadow'; import { ShadowColor } from '../core/colors'; const SHADOW_MATERIAL = new THREE.MeshBasicMaterial({ color: ShadowColor }); export class NoteShadowCube extends NoteShadow { update() { super.update(); this.mesh.scale.setY( Number.EPSILON ); } get geometry() { var cubeGeometry = new THREE.BoxGeometry( 0.1, 0.1, 0.1 ); cubeGeometry.applyMatrix( this.headMesh.matrix ); cubeGeometry.scale( 0.55, 0.55, 0.55 ); return cubeGeometry; } get material() { return SHADOW_MATERIAL; } } ================================================ FILE: js/notes/note-shadow-sphere.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { NoteShadow } from './note-shadow'; const SHADOW_GEOMETRY = new THREE.PlaneGeometry( 0.1, 0.1 ); const SHADOW_SHADER = THREE.CircleShader; const SHADOW_UNIFORMS = THREE.UniformsUtils.clone( SHADOW_SHADER.uniforms ); const SHADOW_MATERIAL = new THREE.ShaderMaterial({ uniforms: SHADOW_UNIFORMS, vertexShader: SHADOW_SHADER.vertexShader, fragmentShader: SHADOW_SHADER.fragmentShader }); export class NoteShadowSphere extends NoteShadow { constructor( headMesh ) { super( headMesh ); this.mesh.rotation.x -= Math.PI / 2; } get geometry() { return SHADOW_GEOMETRY; } get material() { return SHADOW_MATERIAL; } } ================================================ FILE: js/notes/note-shadow-tetra.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { NoteShadow } from './note-shadow'; import { ShadowColor } from '../core/colors'; import { Tetrahedron } from '../util/tetrahedron'; const TETRA_GEOMETRY = new Tetrahedron().geometry; const SHADOW_MATERIAL = new THREE.MeshBasicMaterial({ color: ShadowColor }); export class NoteShadowTetra extends NoteShadow { update() { super.update(); this.mesh.scale.setY( Number.EPSILON ); } get geometry() { var tetraGeometry = TETRA_GEOMETRY.clone(); tetraGeometry.applyMatrix( this.headMesh.matrix ); return tetraGeometry; } get material() { return SHADOW_MATERIAL; } } ================================================ FILE: js/notes/note-shadow.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const SHADOW_SIZE = 1.05; const SHADOW_GROUND_CLEARANCE = 0.01; export class NoteShadow { constructor( headMesh, rotation ) { this.headMesh = headMesh; this.headMesh.updateMatrix(); this.headMesh.updateMatrixWorld( true ); this.rotation = rotation; this.mesh = new THREE.Mesh( this.geometry, this.material ); } update() { var worldZeroVector = new THREE.Vector3( 0, 0, 0 ); this.mesh.parent.worldToLocal( worldZeroVector ); this.mesh.position.setY( worldZeroVector.y + SHADOW_GROUND_CLEARANCE ); this.mesh.scale.copy( this.headMesh.scale ); this.mesh.scale.multiplyScalar( SHADOW_SIZE ); } get geometry() { } get material() { } } ================================================ FILE: js/notes/note-stem.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const STEM_GEOMETRY = new THREE.CylinderGeometry( 0.005, 0.01, 1, 5 ); export class NoteStem { constructor( palette ) { this.palette = palette; this.material = new THREE.MeshBasicMaterial(); var stemMesh = new THREE.Mesh( STEM_GEOMETRY, this.material ); stemMesh.position.y = -0.5; this.mesh = new THREE.Group(); this.mesh.add( stemMesh ); } setTone( value ) { this.tone = value; this.material.color = this.palette.colorPalette()[ this.tone ].idle; this.material.update(); } update() { var worldZeroVector = new THREE.Vector3( 0, 0, 0 ); this.mesh.parent.worldToLocal( worldZeroVector ); this.mesh.position.setY( worldZeroVector.y ); this.mesh.scale.setY( worldZeroVector.y ); } hit() { } } ================================================ FILE: js/notes/note.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { NoteHeadSphere } from './note-head-sphere'; import { NoteHeadTetra } from './note-head-tetra'; import { NoteHeadCube } from './note-head-cube'; import { NoteShadowSphere } from './note-shadow-sphere'; import { NoteShadowTetra } from './note-shadow-tetra'; import { NoteShadowCube } from './note-shadow-cube'; const HIT_ANIM_MS = 750; const SHAPE_NAMES = [ 'sphere', 'cube', 'tetra' ]; const HEAD_CONSTRUCTORS = { sphere: NoteHeadSphere, tetra: NoteHeadTetra, cube: NoteHeadCube }; const SHADOW_CONSTRUCTORS = { sphere: NoteShadowSphere, tetra: NoteShadowTetra, cube: NoteShadowCube }; export class Note { constructor( palette ) { this.group = new THREE.Group(); this.palette = palette; } setTone( value ) { this.tone = value; // Remove old note objects if ( this.head ) this.group.remove( this.head.mesh ); if ( this.shadow ) this.group.remove( this.shadow.mesh ); // Create new head object this.head = new ( HEAD_CONSTRUCTORS[ this.shape ] )( this.palette, this.shape ); this.head.name = "head"; this.head.lightPosition = this.lightPosition; this.head.setTone( this.tone ); // Store first random rotation value if ( !this.rotation ) { this.rotation = new THREE.Euler(); this.rotation.copy( this.head.rotation ); } // Reset head mesh rotation this.head.mesh.rotation.copy( this.rotation ); // Create new shadow object this.shadow = new ( SHADOW_CONSTRUCTORS[ this.shape ] )( this.head.mesh, this.rotation ); // Add 'em up this.group.add( this.head.mesh ); this.group.add( this.shadow.mesh ); } setRotationIncrement(isClockwise=false) { let rotationAxis = new THREE.Vector3(0,1,0); let rotationAmount = Math.PI * 2 * (isClockwise ? 0.03 : -0.03); let deltaMatrix = new THREE.Matrix4(); deltaMatrix.makeRotationAxis(rotationAxis, rotationAmount ); let rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(this.rotation); rotationMatrix.multiplyMatrices(deltaMatrix,rotationMatrix); deltaMatrix.extractRotation(rotationMatrix); this.rotation.setFromRotationMatrix(deltaMatrix); } removeShadow(){ if ( this.shadow ) { this.group.remove( this.shadow.mesh ); delete this.shadow; this.shadow = null; } } tick() { if(this.shadow) { this.shadow.update(); } } highlight( event ) { this.head.highlight( event ); } unHighlight( event ) { this.head.unHighlight( event ); } hit( event ) { this.head.hit( event, HIT_ANIM_MS ); } get shape() { return SHAPE_NAMES[ Math.floor( this.tone / this.palette.noteCount() ) ]; } } ================================================ FILE: js/orientation-arm-model.js ================================================ /* * Copyright 2016 Google Inc. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const HEAD_ELBOW_OFFSET = new THREE.Vector3(0.155, -0.465, -0.15); const ELBOW_WRIST_OFFSET = new THREE.Vector3(0, 0, -0.25); const WRIST_CONTROLLER_OFFSET = new THREE.Vector3(0, 0, 0.05); const ARM_EXTENSION_OFFSET = new THREE.Vector3(-0.08, 0.14, 0.08); const ELBOW_BEND_RATIO = 0.4; // 40% elbow, 60% wrist. const EXTENSION_RATIO_WEIGHT = 0.4; const MIN_ANGULAR_SPEED = 0.61; // 35 degrees per second (in radians). /** * Represents the arm model for the Daydream controller. Feed it a camera and * the controller. Update it on a RAF. * * Get the model's pose using getPose(). */ export default class OrientationArmModel { constructor() { this.isLeftHanded = false; // Current and previous controller orientations. this.controllerQ = new THREE.Quaternion(); this.lastControllerQ = new THREE.Quaternion(); // Current and previous head orientations. this.headQ = new THREE.Quaternion(); // Current head position. this.headPos = new THREE.Vector3(); // Positions of other joints (mostly for debugging). this.elbowPos = new THREE.Vector3(); this.wristPos = new THREE.Vector3(); // Current and previous times the model was updated. this.time = null; this.lastTime = null; // Root rotation. this.rootQ = new THREE.Quaternion(); // Current pose that this arm model calculates. this.pose = { orientation: new THREE.Quaternion(), position: new THREE.Vector3() }; } /** * Methods to set controller and head pose (in world coordinates). */ setControllerOrientation(quaternion) { this.lastControllerQ.copy(this.controllerQ); this.controllerQ.copy(quaternion); } setHeadOrientation(quaternion) { this.headQ.copy(quaternion); } setHeadPosition(position) { this.headPos.copy(position); } setLeftHanded(isLeftHanded) { // TODO(smus): Implement me! this.isLeftHanded = isLeftHanded; } /** * Called on a RAF. */ update() { this.time = performance.now(); // If the controller's angular velocity is above a certain amount, we can // assume torso rotation and move the elbow joint relative to the // camera orientation. let headYawQ = this.getHeadYawOrientation_(); let timeDelta = (this.time - this.lastTime) / 1000; let angleDelta = this.quatAngle_(this.lastControllerQ, this.controllerQ); let controllerAngularSpeed = angleDelta / timeDelta; if (controllerAngularSpeed > MIN_ANGULAR_SPEED) { // Attenuate the Root rotation slightly. this.rootQ.slerp(headYawQ, angleDelta / 10); } else { this.rootQ.copy(headYawQ); } // We want to move the elbow up and to the center as the user points the // controller upwards, so that they can easily see the controller and its // tool tips. let controllerEuler = new THREE.Euler().setFromQuaternion(this.controllerQ, 'YXZ'); let controllerXDeg = THREE.Math.radToDeg(controllerEuler.x); let extensionRatio = this.clamp_((controllerXDeg - 11) / (50 - 11), 0, 1); // Controller orientation in camera space. let controllerCameraQ = this.rootQ.clone().inverse(); controllerCameraQ.multiply(this.controllerQ); // Calculate elbow position. let elbowPos = this.elbowPos; elbowPos.copy(this.headPos).add(HEAD_ELBOW_OFFSET); let elbowOffset = new THREE.Vector3().copy(ARM_EXTENSION_OFFSET); elbowOffset.multiplyScalar(extensionRatio); elbowPos.add(elbowOffset); // Calculate joint angles. Generally 40% of rotation applied to elbow, 60% // to wrist, but if controller is raised higher, more rotation comes from // the wrist. let totalAngle = this.quatAngle_(controllerCameraQ, new THREE.Quaternion()); let totalAngleDeg = THREE.Math.radToDeg(totalAngle); let lerpSuppression = 1 - Math.pow(totalAngleDeg / 180, 4); // TODO(smus): ??? let elbowRatio = ELBOW_BEND_RATIO; let wristRatio = 1 - ELBOW_BEND_RATIO; let lerpValue = lerpSuppression * (elbowRatio + wristRatio * extensionRatio * EXTENSION_RATIO_WEIGHT); let wristQ = new THREE.Quaternion().slerp(controllerCameraQ, lerpValue); let invWristQ = wristQ.inverse(); let elbowQ = controllerCameraQ.clone().multiply(invWristQ); // Calculate our final controller position based on all our joint rotations // and lengths. /* position_ = root_rot_ * ( controller_root_offset_ + 2: (arm_extension_ * amt_extension) + 1: elbow_rot * (kControllerForearm + (wrist_rot * kControllerPosition)) ); */ let wristPos = this.wristPos; wristPos.copy(WRIST_CONTROLLER_OFFSET); wristPos.applyQuaternion(wristQ); wristPos.add(ELBOW_WRIST_OFFSET); wristPos.applyQuaternion(elbowQ); wristPos.add(this.elbowPos); let offset = new THREE.Vector3().copy(ARM_EXTENSION_OFFSET); offset.multiplyScalar(extensionRatio); let position = new THREE.Vector3().copy(this.wristPos); position.add(offset); position.applyQuaternion(this.rootQ); let orientation = new THREE.Quaternion().copy(this.controllerQ); // Set the resulting pose orientation and position. this.pose.orientation.copy(orientation); this.pose.position.copy(position); this.lastTime = this.time; } /** * Returns the pose calculated by the model. */ getPose() { return this.pose; } /** * Debug methods for rendering the arm model. */ getForearmLength() { return ELBOW_WRIST_OFFSET.length(); } getElbowPosition() { let out = this.elbowPos.clone(); return out.applyQuaternion(this.rootQ); } getWristPosition() { let out = this.wristPos.clone(); return out.applyQuaternion(this.rootQ); } getHeadYawOrientation_() { let headEuler = new THREE.Euler().setFromQuaternion(this.headQ, 'YXZ'); headEuler.x = 0; headEuler.z = 0; let destinationQ = new THREE.Quaternion().setFromEuler(headEuler); return destinationQ; } clamp_(value, min, max) { return Math.min(Math.max(value, min), max); } quatAngle_(q1, q2) { let vec1 = new THREE.Vector3(0, 0, -1); let vec2 = new THREE.Vector3(0, 0, -1); vec1.applyQuaternion(q1); vec2.applyQuaternion(q2); return vec1.angleTo(vec2); } } ================================================ FILE: js/shaders/ball-shader.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { CommonUniforms, ToonVertex, ToonFragUniforms, ToonFragCommon, ToonFragLighting } from './shader-chunks'; THREE.BallShader = { uniforms: THREE.UniformsUtils.merge([ CommonUniforms, { map134: { type: 't' }, map567: { type: 't' }, use567: { value: 0.0 }, spriteLayout: { value: new THREE.Vector2( 8, 4 ) }, spriteRepeat: { value: new THREE.Vector2( 5, 1 ) }, spriteChannel: { value: new THREE.Vector3( 1, 0, 0 ) }, spriteIndex: { value: 0 }, bgColor: { value: new THREE.Color( 'rgb( 53, 101, 190 )' ) }, idleColor: { value: new THREE.Color( 'rgb( 0, 55, 182 )' ) }, activeColor: { value: new THREE.Color( 'rgb( 0, 130, 237 )' ) }, activeAmount: { value: 0 }, brightnessAmount: { value: 0.0 } } ]), vertexShader: ToonVertex, fragmentShader: [ ToonFragUniforms, 'uniform sampler2D map134;', 'uniform sampler2D map567;', 'uniform float use567;', 'uniform vec2 spriteLayout;', 'uniform vec2 spriteRepeat;', 'uniform vec3 spriteChannel;', 'uniform float spriteIndex;', 'uniform vec3 bgColor;', 'uniform vec3 idleColor;', 'uniform vec3 activeColor;', 'uniform float activeAmount;', 'uniform float brightnessAmount;', 'void main() {', ToonFragLighting, 'vec2 uvSpriteOffset = vec2( 0.0 );', 'uvSpriteOffset.x = floor( mod( spriteIndex, spriteLayout.x ) );', 'uvSpriteOffset.y = -floor( spriteIndex / spriteLayout.x ) - 1.0;', 'uvSpriteOffset /= spriteLayout;', 'vec2 uvSize = 1.0 / spriteLayout;', 'vec2 vUvSprite = vUv;', 'vUvSprite.y = vUvSprite.y + spriteLayout.y - 1.0;', 'vUvSprite = mod( vUvSprite * uvSize * spriteRepeat, uvSize ) + uvSpriteOffset;', 'vec3 tex134 = texture2D( map134, vUvSprite ).rgb;', 'vec3 tex567 = texture2D( map567, vUvSprite ).rgb;', 'vec3 a = mix( tex134, tex567, use567 );', 'vec3 color = mix( idleColor, activeColor, activeAmount );', 'diffuseColor = vec4( mix( bgColor, color, length( a * spriteChannel ) ) + brightnessAmount, 1.0 );', ToonFragCommon, '}' ].join( '\n' ) }; ================================================ FILE: js/shaders/bg-tree-shader.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { BgTreeColors, ShadowColor } from '../core/colors'; THREE.BGTreeShader = { uniforms: { map: { type: 't' }, fogColor: { type: 'c' }, fogDensity: { value: 0 }, color: { type: 'c', value: new THREE.Color( BgTreeColors[ 0 ] ) }, shadowColor: { type: 'c', value: new THREE.Color( ShadowColor ) } }, vertexShader: [ 'varying vec2 vUV;', 'varying float fogDepth;', 'void main() {', 'vUV = uv;', 'vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );', 'fogDepth = -mvPosition.z;', 'gl_Position = projectionMatrix * mvPosition;', '}' ].join( '\n' ), fragmentShader: [ '#define LOG2 1.442695', 'uniform vec3 color;', 'uniform vec3 shadowColor;', 'uniform sampler2D map;', 'uniform float fogDensity;', 'uniform vec3 fogColor;', 'varying float fogDepth;', 'varying vec2 vUV;', 'void main() {', 'vec3 c = vUV.y > 0.5 ? color : shadowColor;', 'vec3 t = texture2D( map, vUV ).rgb;', 'if ( t.r < 0.5 ) discard;', 'float fogFactor = 1.0 - saturate( ( exp2( -fogDensity * fogDensity * fogDepth * fogDepth * LOG2 ) ) );', 'gl_FragColor = vec4( mix( t * c, fogColor, fogFactor ), 1.0 );', '}' ].join( '\n' ) }; ================================================ FILE: js/shaders/circle-shader.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { ShadowColor } from '../core/colors'; import { BasicVertex } from './shader-chunks'; THREE.CircleShader = { uniforms: { radius: { type: 'f', value: 0.5 }, color: { type: 'c', value: new THREE.Color( ShadowColor ) } }, vertexShader: BasicVertex, fragmentShader: [ 'uniform float radius;', 'uniform vec3 color;', 'varying vec2 vUV;', 'void main() {', 'if ( length( vUV - 0.5 ) > radius ) discard;', 'gl_FragColor = vec4( color, 1.0 );', '}' ].join( '\n' ) }; ================================================ FILE: js/shaders/shader-chunks.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const CommonUniforms = { lightPosition: { value: new THREE.Vector3( 3, 10, 1 ) }, lightIntensity: { value: 1.15 }, shadeAmount: { value: 0.55 } }; const BasicVertex = [ 'varying vec2 vUV;', 'void main() {', 'vUV = uv;', 'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', '}' ].join( '\n' ); const ToonVertex = [ 'varying vec4 vWorldPosition;', 'varying vec3 vNormal;', 'varying vec2 vUv;', 'void main() {', 'vUv = uv;', // World-space normal 'vNormal = normalize ( mat3( modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz ) * normal );', 'vec3 transformed = vec3( position );', 'vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );', 'gl_Position = projectionMatrix * mvPosition;', 'vWorldPosition = modelMatrix * vec4( position, 1.0 );', '}' ].join( '\n' ); const ToonFragUniforms = [ 'uniform float lightIntensity;', 'uniform float shadeAmount;', 'uniform vec3 lightPosition;', 'uniform sampler2D gradientMap;', 'varying vec4 vWorldPosition;', 'varying vec3 vNormal;', 'varying vec2 vUv;', ].join( '\n' ); const ToonFragLighting = [ '#ifdef FLAT_SHADED', // TODO: do this in the vertex shader for speeeeeed 'vec3 fdx = vec3( dFdx( vWorldPosition.x ), dFdx( vWorldPosition.y ), dFdx( vWorldPosition.z ) );', 'vec3 fdy = vec3( dFdy( vWorldPosition.x ), dFdy( vWorldPosition.y ), dFdy( vWorldPosition.z ) );', 'vec3 normal = normalize( cross( fdx, fdy ) );', '#else', 'vec3 normal = normalize( vNormal );', '#endif', // The usual Lambertian stuff 'vec3 lightDirection = normalize( lightPosition - vWorldPosition.xyz );', 'float dotNL = max( dot( normal, lightDirection ), 0.0 ) * 0.5 + 0.5;', // Clamp for a shaded toon look 'float toonIrradience = ( dotNL < shadeAmount ) ? 0.7 * lightIntensity : 1.0;', 'vec4 diffuseColor = vec4( 0.0 );', ].join( '\n' ); const ToonFragCommon = [ 'gl_FragColor = vec4( diffuseColor.rgb * toonIrradience, diffuseColor.a );', ].join( '\n' ); export { CommonUniforms, BasicVertex, ToonVertex, ToonFragUniforms, ToonFragCommon, ToonFragLighting }; ================================================ FILE: js/splash.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the 'License'); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an 'AS IS' BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import TWEEN from 'tween.js'; import StartAudioContext from 'startaudiocontext'; import Tone from 'tone'; import { getParameterByName, getViewerType } from './util'; const getComponentProperty = AFRAME.utils.entity.getComponentProperty; const setComponentProperty = AFRAME.utils.entity.setComponentProperty; const stringify = AFRAME.utils.coordinates.stringify; AFRAME.registerComponent('splash', { flyTweenPosition:undefined, flyTweenRotation:undefined, isFirstTime:true, viewer : false, mode:undefined, camPos:{ x:0, y:0, z:0 }, init(){ if(WebVRConfig){ WebVRConfig.CARDBOARD_UI_DISABLED = false; WebVRConfig.ENABLE_DEPRECATED_API = true; WebVRConfig.ROTATE_INSTRUCTIONS_DISABLED = false; } }, play(){ this._splashEl = document.querySelector('#splash'); this._splashEl.querySelector('#enterButton').appendChild(document.querySelector('.webvr-ui-button')); // listen for enter vr this.el.addEventListener('enter-vr', () => this._entered('vr')); this.el.addEventListener('exit-vr', () => this._exited()); // start audio context StartAudioContext(Tone.context, ['.webvr-ui-button', '#enter-360']); // Connect to server on loaded if( getParameterByName('autojoin') ){ setTimeout(() => this._entered('360'), 1000); } // add's editing capbaility on desktop if( getParameterByName('reticle')==='true' ){ let avatar = document.querySelector('#avatar'); setComponentProperty(avatar,'grab-move', ''); } let displayId; //send analytics events let enterVRButton = this.el.components['webvr-ui'].enterVR; enterVRButton.on('error', (e)=>{ ga('send', 'event', 'device', 'error', e); }).on('ready', ()=>{ enterVRButton.getVRDisplay().then((disp)=>{ displayId = disp.displayId; ga('send', 'event', 'device', 'ready', disp.displayName); }); }); window.addEventListener('gamepadconnected', (e)=>{ let gamepads = navigator.getGamepads(); for (let i = 0; i < gamepads.length; ++i) { if(gamepads[i]){ // if(gamepads[i] && gamepads[i].displayId == displayId){ ga('send', 'event', 'gamepad', 'ready', gamepads[i].id); } } }); this._progressBar(); }, _progressBar(){ const promises = []; let asset; for(let i=0; i { asset.addEventListener('loaded', done); })); } } promises.push(new Promise(done => { Tone.Buffer.on('load', done); })); this._allLoaded(); }, _allLoaded(){ // let avatar = document.querySelector('#avatar'); let camera = document.querySelector('a-entity[camera]'); let concater = function(obj) { Object.keys(obj).forEach( key => { obj[key] = ((obj[key]*100) | 0) / 100; }); return JSON.stringify(obj); }; /* * HACK: position is stored to counteract height issues * associated with Oculus's relative position data * using componentchanged instead of EXIT VR */ camera.addEventListener('componentchanged', (event) => { if ( event.detail.name === 'position' || event.detail.name === 'rotation' ) { let pos = getComponentProperty(camera,'position'); if (pos.y !== 1.6 && pos.y !== 0){ this.camPos.x = pos.x; this.camPos.y = pos.y; this.camPos.z = pos.z; } } }); this._splashEl.querySelector('#enter-container').classList.add('loaded'); this._splashEl.querySelector('#enter-360').addEventListener('click', () => this._entered('360')); let treeCreatedEvent = (event) => { document.removeEventListener('TREE_CREATED', treeCreatedEvent); this._animateToTree(); }; document.addEventListener('TREE_CREATED', treeCreatedEvent); }, _animateToTree(){ const player = document.querySelector('#player'); const avatar = document.querySelector('#avatar'); const camera = document.querySelector('a-entity[camera]'); const delay = 500; const speed = 1000; let position = getComponentProperty(player,'position'); let rotation = getComponentProperty(player,'rotation'); let isTabletWindow = this.isTabletLikeDimensions() && this.isTouchDevice() && this.mode === '360'; let isTabletVR = this.isTabletLikeDimensions() && this.isTouchDevice() && this.mode === 'vr'; let isDesktop = this.mode === '360' && !AFRAME.utils.device.isMobile(); let y = (isDesktop) ? 1.6: 0; y = (isTabletWindow) ? 0: y; const z = this.viewer ? 1.5 : 0; this.flyTweenPosition = new TWEEN.Tween(position) .to({ x:0, y:y, z:z }, speed) .delay(delay) .easing(TWEEN.Easing.Quadratic.InOut) .onUpdate(() => player.setAttribute('position', position)); this.flyTweenRotation = new TWEEN.Tween(rotation) .to({ x:0, y: 0, z: 0 }, speed) .delay(delay) .easing(TWEEN.Easing.Quadratic.InOut) .onUpdate(() => player.setAttribute('rotation', rotation)) .onComplete(() => { this.isFirstTime = false; document.dispatchEvent(new Event('INTRO_COMPLETED')); }); if (this.flyTweenPosition) { this.flyTweenPosition.stop(); } if (this.flyTweenRotation) { this.flyTweenRotation.stop(); } this.flyTweenPosition.start(); this.flyTweenRotation.start(); }, _exited(){ this._splashEl.classList.remove('invisible'); document.dispatchEvent(new Event('EXITED_FOREST')); this.removeCardBoardInstructions(); this.mode = undefined; }, _entered(mode){ this.mode = mode; this.addCardBoardInstructions(); if(this.mode === '360') { this.viewer = true; ga('send', 'event', 'clickable_link', 'splash_page', 'enter_360'); } else { ga('send', 'event', 'clickable_link', 'splash_page', 'enter_VR'); } let player = document.querySelector('#player'); let avatar = document.querySelector('#avatar'); let camera = document.querySelector('a-entity[camera]'); if(!this.isFirstTime && !AFRAME.utils.device.isMobile()){ setComponentProperty(player,'position', '0 0 0'); setComponentProperty(avatar,'position', this.camPos); setComponentProperty(camera,'position', this.camPos); } else if (!this.isFirstTime){ setComponentProperty(player,'position', '0 0 0'); setComponentProperty(avatar,'position', '0 1.6 0'); setComponentProperty(camera,'position', '0 1.6 0'); } this.el.removeEventListener('enter-vr', () => this._entered()); this._splashEl.querySelector('#enter-360').removeEventListener('click', () => this._entered()); this._splashEl.classList.add('invisible'); this.el.sceneEl.systems['copresence-server'].connectToServer(); // add ux based on device getViewerType((clientType) => { this._setSceneParameters({ clientType: clientType, mode: this.mode, isMobile: AFRAME.utils.device.isMobile(), }); document.dispatchEvent(new Event('ENTERED_FOREST')); }); }, _setSceneParameters(params){ let avatar = document.querySelector('#avatar'); let player = document.querySelector('#player'); let camera = document.querySelector('a-entity[camera]'); let floor = document.querySelector('#gaze-floor'); let floorStatic = document.querySelector('#floorStatic'); let skyStatic = document.querySelector('#skyStatic'); let isTabletWindow = this.isTabletLikeDimensions() && this.isTouchDevice() && this.mode === '360'; let isTabletVR = this.isTabletLikeDimensions() && this.isTouchDevice() && this.mode === 'vr'; let is6DOF = params.clientType === '6dof'; let isDesktop = params.clientType === 'viewer' && !params.isMobile; let is3DOF = params.clientType === '3dof'; let isMagicWindow = params.isMobile && this.mode === '360'; let isCardBoard = params.isMobile && this.mode === 'vr'; if(isTabletVR){ setComponentProperty(avatar, 'position', '0 1.6 0'); setComponentProperty(floor,'teleport', ''); setComponentProperty(avatar,'look-controls', ''); setComponentProperty(avatar,'clicker', ''); } else if(isTabletWindow){ setComponentProperty(avatar, 'position', '0 1.6 0'); setComponentProperty(avatar, 'rotation', '-35 0 0'); setComponentProperty(floor,'teleport', ''); setComponentProperty(avatar,'look-controls', ''); setComponentProperty(avatar,'clicker', ''); } else if(is6DOF){ setComponentProperty(avatar,'look-controls', ''); setComponentProperty(avatar,'clicker', ''); setComponentProperty(floorStatic,'material', 'shader: flat; color: #e9a69a;'); } else if (isDesktop){ setComponentProperty(avatar, 'camera', 'userHeight:0.0'); setComponentProperty(player,'look-controls', ''); setComponentProperty(player,'wasd-controls', 'acceleration:35; easing:25;'); setComponentProperty(player,'wasd-boundaries', 'maxRadius:3.125;'); setComponentProperty(avatar,'clicker', ''); setComponentProperty(floorStatic,'material', 'shader: flat; color: #e9a69a;'); } else if (is3DOF) { setComponentProperty(floor,'teleport', ''); setComponentProperty(avatar,'look-controls', ''); setComponentProperty(avatar,'clicker', ''); } else if (isMagicWindow){ this.removeCardBoardInstructions(); setComponentProperty(floor, 'teleport', ''); setComponentProperty(avatar,'look-controls', ''); setComponentProperty(avatar,'clicker', ''); } else if (isCardBoard){ setComponentProperty(floor, 'teleport', ''); setComponentProperty(avatar,'look-controls', ''); } else { } }, removeCardBoardInstructions(){ let cardboard = document.querySelector("#cardboard"); if(cardboard) { cardboard.remove(); } }, addCardBoardInstructions(){ if(AFRAME.utils.device.isMobile() && this.mode === 'vr'){ let cardboard = document.createElement('div'); cardboard.id = 'cardboard'; document.body.appendChild(cardboard); // createElement let container = document.createElement('div'); container.id = 'cardboardContainer'; cardboard.appendChild(container); let img = document.createElement("img"); img.id = "img"; img.src = `/static/img/cardboardInstructions.gif`; container.appendChild(img); let p = document.createElement("p"); p.innerHTML = 'Place your phone into your default viewer

'; container.appendChild(p); let p2 = document.createElement("p"); p2.innerHTML = 'No Cardboard viewer?'; container.appendChild(p2); let b = document.createElement("button"); b.innerHTML = `GET ONE`; container.appendChild(b); b.addEventListener("click", () => { window.open("https://vr.google.com/cardboard/get-cardboard/", '_blank'); }); } }, isTabletLikeDimensions() { let w = screen.availWidth; let h = screen.availHeight; return ( ( (w > h) ? w : h ) >= 960 ); }, isTouchDevice() { return 'ontouchstart' in window || navigator.maxTouchPoints; // works on most browsers || works on IE10/11 and Surface }, }); function tweenUpdate() { requestAnimationFrame(tweenUpdate); TWEEN.update(); } tweenUpdate(); (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); ga('create', 'UA-90331006-2', 'auto'); ga('send', 'pageview'); let aboutBTN = document.getElementById('about-button'); let about = document.getElementById('about'); aboutBTN.addEventListener('click', () => { about.classList.add('visible'); aboutBTN.classList.remove('visible'); ga('send', 'event', 'clickable_link', 'splash_page', 'show_info'); }); about.addEventListener('click', () => { about.classList.remove('visible'); aboutBTN.classList.add('visible'); }); ================================================ FILE: js/util/browserCheck.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { showErrorMessage } from '../util'; if(navigator.userAgent.indexOf('MSIE')!==-1 || navigator.appVersion.indexOf('Trident/') > 0) { showErrorMessage("error_shake.gif", "Your browser is not supported","Download Chrome","http://www.google.com/chrome"); } ================================================ FILE: js/util/random-range-1d.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. export class RandomRange { constructor( min, max ) { this.min = min; this.max = max; } get [ 'value' ]() { return Math.random() * ( this.max - this.min ) + this.min; } } ================================================ FILE: js/util/random-range-3d.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { RandomRange } from './random-range-1d'; export class RandomRange3D { constructor( min, max ) { this.type = min.constructor; this.range3D = { x: new RandomRange( min.x, max.x ), y: new RandomRange( min.y, max.y ), z: new RandomRange( min.z, max.z ) }; } get x() { return this.range3D.x.value; } get y() { return this.range3D.y.value; } get z() { return this.range3D.z.value; } get [ 'value' ]() { return new this.type( this.x, this.y, this.z ); } } ================================================ FILE: js/util/tetrahedron.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const X = Math.sqrt( 2 / 3 ); const Y = 1 / 3; const Z = 1 / Math.sqrt( 2 ) * ( 2 / 3); const v1 = new THREE.Vector3( -X, -Y, Z ); const v2 = new THREE.Vector3( +X, -Y, Z ); const v3 = new THREE.Vector3( 0, -Y, -Z * 2 ); const v4 = new THREE.Vector3( 0, 1, 0 ); const f1 = new THREE.Face3( 2, 1, 0 ); const f2 = new THREE.Face3( 1, 3, 0 ); const f3 = new THREE.Face3( 2, 3, 1 ); const f4 = new THREE.Face3( 0, 3, 2 ); const t1 = new THREE.Vector2( 1, 0 ); const t2 = new THREE.Vector2( 0.5, 1 ); const t3 = new THREE.Vector2( 0, 0 ); const uvs = [ [ t1, t2, t3 ], [ t1, t2, t3 ], [ t1, t2, t3 ], [ t1, t2, t3 ] ]; export class Tetrahedron { constructor() { this.geometry = new THREE.Geometry(); this.geometry.vertices.push( v1, v2, v3, v4 ); this.geometry.faces.push( f1, f2, f3, f4 ); this.geometry.faceVertexUvs = [ uvs ]; this.geometry.scale( 0.32, 0.32, 0.32 ); this.geometry.uvsNeedUpdate = true; // Compute normals automatically this.geometry.computeFaceNormals(); this.geometry.computeVertexNormals(); } } ================================================ FILE: js/util/trace.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. if (typeof console._commandLineAPI !== 'undefined') { console.API = console._commandLineAPI; //chrome } else if (typeof console._inspectorCommandLineAPI !== 'undefined') { console.API = console._inspectorCommandLineAPI; //Safari } else if (typeof console.clear !== 'undefined') { console.API = console; } window.clear = console.API.clear; window.trace = console.log; ================================================ FILE: js/util.js ================================================ // Copyright 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. export function getParameterByName(name, url) { if (!url) { url = window.location.href; } name = name.replace(/[\[\]]/g, "\\$&"); let regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), results = regex.exec(url); if (!results) return null; if (!results[2]) return ''; return decodeURIComponent(results[2].replace(/\+/g, " ")); } export function scale(value, inMin, inMax, outMin, outMax) { return ((value - inMin) / (inMax - inMin)) * (outMax - outMin) + outMin; } export function getViewerType(callBack){ navigator.getVRDisplays() .then( function( displays ) { if(displays.length>0 && displays[0].isPresenting) { if (displays[0].stageParameters === null) { callBack('3dof'); } else { callBack('6dof'); } } else { callBack('viewer'); } }) .catch( function() { callBack('viewer'); }); } export function showErrorMessage(imgURL, errorCode, cta, ctaUrl){ console.error(errorCode); let error = document.querySelector("#error"); // delete error if it already exists if(error) { error.remove(); } // createElement error = document.createElement('div'); error.id = 'error'; document.body.appendChild(error); let container = document.createElement('div'); container.id = 'errorContainer'; error.appendChild(container); let img = document.createElement("img"); img.id = "img"; img.src = `/static/img/${imgURL}`; container.appendChild(img); let p = document.createElement("p"); let notification = `${errorCode}`; p.appendChild(document.createTextNode(notification)); container.appendChild(p); let b = document.createElement("button"); b.innerHTML = `${cta}`; container.appendChild(b); b.addEventListener("click", () => { if(ctaUrl){ window.open(ctaUrl, '_blank'); } else { window.location.href = window.location.href.replace(/#.*/,'#'); error.remove(); let splash = document.querySelector('#splash'); splash.classList.remove('invisible'); } }); } window.showErrorMessage = showErrorMessage; ================================================ FILE: package.json ================================================ { "name": "webvr-musical-forest", "version": "0.0.1", "description": "", "authors": [], "scripts": { "start": "npm run build-css && npm run budo", "build": "browserify js/index.js | derequire > build/main.js", "buildmin": "browserify -g uglifyify js/index.js | derequire > build/main.js", "build-css": "node-sass --include-path scss style/splash.scss build/style.css", "budo": "budo js/index.js:build/main.js --live --verbose --port 3000", "appengine": "~/bin/google_appengine/dev_appserver.py app.yaml", "appengine_watch": "parallelshell 'npm run appengine' 'npm run watch'", "watch": "watchify js/index.js -v -d -o build/main.js", "discify": "browserify --full-paths js/index.js | derequire | discify --open" }, "repository": { "type": "git", "url": "https://github.com/googlecreativelab/webvr-musicalforest" }, "dependencies": { "aframe": "^0.5.0", "bufferutil": "^1.2.1", "domready": "^1.0.8", "envify": "^4.0.0", "image-promise": "^4.0.1", "startaudiocontext": "^1.2.0", "tone": "^0.9.0", "uglifyify": "^3.0.4", "underscore": "^1.8.3", "url-parse": "^1.1.6", "webvr-polyfill": "aframevr/webvr-polyfill", "webvr-ui": "^0.9.4" }, "devDependencies": { "babel-preset-es2015": "^6.16.0", "babel-preset-stage-2": "^6.22.0", "babelify": "^7.3.0", "browserify": "^14.0.0", "budo": "^9.4.7", "derequire": "^2.0.3", "discify": "^1.6.0", "node-sass": "^4.5.2", "parallelshell": "^2.0.0", "utf-8-validate": "^1.2.1", "watchify": "^3.7.0" }, "keywords": [ "vr", "webvr" ], "license": "Apache-2.0", "browserify": { "transform": [ [ "babelify", { "presets": [ "es2015" ] } ] ] } } ================================================ FILE: python/__init__.py ================================================ ================================================ FILE: python/base/__init__.py ================================================ ================================================ FILE: python/base/api_fixer.py ================================================ # Copyright 2014 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Fixes up various popular APIs to ensure they use secure defaults.""" import __builtin__ import constants import cPickle import functools import io import json import logging import pickle import yaml from google.appengine.api import urlfetch from webapp2_extras import sessions class ApiSecurityException(Exception): """Error when attempting to call an unsafe API.""" pass def FindArgumentIndex(function, argument): args = function.func_code.co_varnames[:function.func_code.co_argcount] return args.index(argument) def GetDefaultArgument(function, argument): argument_index = FindArgumentIndex(function, argument) num_positional_args = (function.func_code.co_argcount - len(function.func_defaults)) default_position = argument_index - num_positional_args if default_position < 0: return None return function.func_defaults[default_position] def ReplaceDefaultArgument(function, argument, replacement): argument_index = FindArgumentIndex(function, argument) num_positional_args = (function.func_code.co_argcount - len(function.func_defaults)) default_position = argument_index - num_positional_args if default_position < 0: raise ApiSecurityException('Attempt to modify positional default value') new_defaults = list(function.func_defaults) new_defaults[default_position] = replacement function.func_defaults = tuple(new_defaults) # JSON. # Does not escape HTML metacharacters by default. _JSON_CHARACTER_REPLACEMENT_MAPPING = [ ('<', '\\u%04x' % ord('<')), ('>', '\\u%04x' % ord('>')), ('&', '\\u%04x' % ord('&')), ] class _JsonEncoderForHtml(json.JSONEncoder): def encode(self, o): chunks = self.iterencode(o, _one_shot=True) if not isinstance(chunks, (list, tuple)): chunks = list(chunks) return ''.join(chunks) def iterencode(self, o, _one_shot=False): chunks = super(_JsonEncoderForHtml, self).iterencode(o, _one_shot) for chunk in chunks: for (character, replacement) in _JSON_CHARACTER_REPLACEMENT_MAPPING: chunk = chunk.replace(character, replacement) yield chunk ReplaceDefaultArgument(json.dump, 'cls', _JsonEncoderForHtml) ReplaceDefaultArgument(json.dumps, 'cls', _JsonEncoderForHtml) # Pickle. See http://www.cs.jhu.edu/~s/musings/pickle.html for more info. # Whitelist of module name => (module, [list of safe names]) _PICKLE_CLASS_WHITELIST = { '__builtin__': (__builtin__, ['basestring', 'bool', 'buffer', 'bytearray', 'bytes', 'complex', 'dict', 'enumerate', 'float', 'frozenset', 'int', 'list', 'long', 'reversed', 'set', 'slice', 'str', 'tuple', 'unicode', 'xrange']), } # See https://docs.python.org/3/library/pickle.html#restricting-globals. class RestrictedUnpickler(pickle.Unpickler): def find_class(self, module_name, name): (module, safe_names) = _PICKLE_CLASS_WHITELIST.get(module_name, (None, [])) if name in safe_names: return getattr(module, name) raise ApiSecurityException('%s.%s forbidden in unpickling' % (module, name)) def _SafePickleLoad(f): return RestrictedUnpickler(f).load() def _SafePickleLoads(string): return RestrictedUnpickler(io.BytesIO(string)).load() pickle.load = _SafePickleLoad pickle.loads = _SafePickleLoads cPickle.load = _SafePickleLoad cPickle.loads = _SafePickleLoads # YAML. The Python tag scheme allows arbitrary code execution: # yaml.load('!!python/object/apply:os.system ["ls"]') ReplaceDefaultArgument(yaml.compose, 'Loader', yaml.loader.SafeLoader) ReplaceDefaultArgument(yaml.compose_all, 'Loader', yaml.loader.SafeLoader) ReplaceDefaultArgument(yaml.load, 'Loader', yaml.loader.SafeLoader) ReplaceDefaultArgument(yaml.load_all, 'Loader', yaml.loader.SafeLoader) ReplaceDefaultArgument(yaml.parse, 'Loader', yaml.loader.SafeLoader) ReplaceDefaultArgument(yaml.scan, 'Loader', yaml.loader.SafeLoader) # AppEngine urlfetch. # Does not validate certificates by default. ReplaceDefaultArgument(urlfetch.fetch, 'validate_certificate', True) ReplaceDefaultArgument(urlfetch.make_fetch_call, 'validate_certificate', True) def _HttpUrlLoggingWrapper(func): """Decorates func, logging when 'url' params do not start with https://.""" @functools.wraps(func) def _CheckAndLog(*args, **kwargs): try: arg_index = FindArgumentIndex(func, 'url') except ValueError: return func(*args, **kwargs) if arg_index < len(args): arg_value = args[arg_index] elif 'url' in kwargs: arg_value = kwargs['url'] elif 'url' not in kwargs: arg_value = GetDefaultArgument(func, 'url') if arg_value and not arg_value.startswith('https://'): logging.warn('SECURITY : fetching non-HTTPS url %s' % (arg_value)) return func(*args, **kwargs) return _CheckAndLog urlfetch.fetch = _HttpUrlLoggingWrapper(urlfetch.fetch) urlfetch.make_fetch_call = _HttpUrlLoggingWrapper(urlfetch.make_fetch_call) # webapp2_extras session does not set HttpOnly/Secure by default. sessions.default_config['cookie_args']['secure'] = (not constants.IS_DEV_APPSERVER) sessions.default_config['cookie_args']['httponly'] = True ================================================ FILE: python/base/api_fixer_test.py ================================================ # Copyright 2014 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Tests for base.api_fixer.""" import json import pickle import unittest2 import yaml import api_fixer class BadPickle(object): """Dummy object.""" def __reduce__(self): return tuple([eval, tuple(['[1][2]'])]) class ApiFixerTest(unittest2.TestCase): """Test cases for base.api_fixer.""" def testJsonEscaping(self): o = {'foo': ''} self.assertFalse('<' in json.dumps(o)) def testYamlLoading(self): unsafe = '!!python/object/apply:os.system ["ls"]' try: yaml.load(unsafe) self.fail('loading unsafe YAML object succeeded') except yaml.constructor.ConstructorError: pass def testPickle(self): b = { 'foo': BadPickle() } s = pickle.dumps(b) try: b = pickle.loads(s) self.fail('BadPickle() loaded successfully') except IndexError: self.fail('pickled code execution') except api_fixer.ApiSecurityException: pass foo = { 'bar': [1, 2, 3] } s = pickle.dumps(foo) try: foo = pickle.loads(s) self.assertEqual(foo['bar'][0], 1) except Exception: self.fail('safe unpickling failed') if __name__ == '__main__': unittest2.main() ================================================ FILE: python/base/constants.py ================================================ # Copyright 2014 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Public constants for use in application configuration.""" import os def _IsDevAppServer(): return os.environ.get('SERVER_SOFTWARE', '').startswith('Development') # CSP Nonce length NONCE_LENGTH = 10 # webapp2 application configuration constants. # template (CLOSURE, DJANGO, JINJA2) = range(0, 3) # using_angular DEFAULT_ANGULAR = False # framing_policy (DENY, SAMEORIGIN, PERMIT) = range(0, 3) X_FRAME_OPTIONS_VALUES = {DENY: 'DENY', SAMEORIGIN: 'SAMEORIGIN'} # hsts_policy DEFAULT_HSTS_POLICY = {'max_age': 2592000, 'includeSubdomains': True} # placeholder for the CSP nonce. 'nonce_value' is replaced for every response # in base/handers.py with a random nonce value. CSP_NONCE_PLACEHOLDER_FORMAT = '\'nonce-%(nonce_value)s\' ' # IS_DEV_APPSERVER is primarily used for decisions that rely on whether or # not the application is currently serving over HTTPS (dev_appserver does # not support HTTPS). IS_DEV_APPSERVER = _IsDevAppServer() DEBUG = IS_DEV_APPSERVER TEMPLATE_DIR = os.path.sep.join([os.path.dirname(__file__), '..', '..']) # csp_policy DEFAULT_CSP_POLICY = { # Disallow Flash, etc. 'object-src': '\'none\'', # Strict CSP with fallbacks for browsers not supporting CSP v3. 'script-src': CSP_NONCE_PLACEHOLDER_FORMAT + # Propagate trust to dynamically created scripts. '\'strict-dynamic\' ' # Fallback. Ignored in presence of a nonce '\'unsafe-inline\' ' # Fallback. Ignored in presence of strict-dynamic. 'https: http:', 'report-uri': '/csp', 'reportOnly': DEBUG, } ================================================ FILE: python/base/handlers.py ================================================ # Copyright 2014 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """A collection of secure base handlers for webapp2-based applications.""" import abc import base64 import django.conf import django.template import django.template.loader import functools import json import webapp2 from webapp2_extras import jinja2 import api_fixer import constants import models import os import xsrf from google.appengine.api import memcache from google.appengine.api import users # Django initialization. django.conf.settings.configure(DEBUG=constants.DEBUG, TEMPLATE_DEBUG=constants.DEBUG, TEMPLATE_DIRS=[constants.TEMPLATE_DIR]) # Assorted decorators that can be used inside a webapp2.RequestHandler object # to assert certain preconditions before entering any method. def requires_auth(f): """A decorator that requires a currently logged in user.""" @functools.wraps(f) def wrapper(self, *args, **kwargs): if not users.get_current_user(): self.DenyAccess() else: return f(self, *args, **kwargs) return wrapper def requires_admin(f): """A decorator that requires a currently logged in administrator.""" @functools.wraps(f) def wrapper(self, *args, **kwargs): if not users.is_current_user_admin(): self.DenyAccess() else: return f(self, *args, **kwargs) return wrapper def xsrf_protected(f): """Decorator to validate XSRF tokens for any verb but GET, HEAD, OPTIONS.""" @functools.wraps(f) def wrapper(self, *args, **kwargs): non_xsrf_protected_verbs = ['options', 'head', 'get'] if (self.request.method.lower() in non_xsrf_protected_verbs or self._RequestContainsValidXsrfToken()): return f(self, *args, **kwargs) else: self.XsrfFail() return wrapper # Utility functions. def _GetXsrfKey(): """Returns the current key for generating and verifying XSRF tokens.""" client = memcache.Client() xsrf_key = client.get('xsrf_key') if not xsrf_key: config = models.GetApplicationConfiguration() xsrf_key = config.xsrf_key client.set('xsrf_key', xsrf_key) return xsrf_key def _GetCspNonce(): """Returns a random CSP nonce.""" nonce_length = constants.NONCE_LENGTH return base64.b64encode(os.urandom(nonce_length * 2))[:nonce_length] # Classes with a __metaclass__ of _HandlerMeta may not contain any methods # with these names. This is checked when the class is instantiated. _RESTRICTED_FUNCTION_LIST = [ 'dispatch', '_RequestContainsValidXsrfToken', ] # Classes with these names (and _HandlerMeta as a metaclass) can contain # functions in the _RESTRICTED_FUNCTION_LIST. Note that there is no # package/module specified, so it is possible to bypass this check through # clever (or malicious) naming. _RESTRICTED_FUNCTION_CLASS_WHITELIST = [ 'BaseHandler', 'BaseAjaxHandler', 'BaseCronHandler', 'BaseTaskHandler', 'AuthenticatedHandler', 'AuthenticatedAjaxHandler', 'AdminHandler', 'AdminAjaxHandler', ] # This prefix is returned on GET requests to any Ajax-like handler. # It is used to prevent JSON-like responses that may contain non-public # information from being included in malicious domains, e.g. evil.com # inserting a tag like: . # evil.com cannot strip this prefix before parsing the result, unlike # same-origin requests. See https://google-gruyere.appspot.com/part3 # for more information. It is not necessary for POST requests because # there is no way to force the browser to make a cross-domain POST request # and interpret the response as Javascript without use of other mechanisms # like Cross-Origin-Resource-Sharing, which is disabled by default. _XSSI_PREFIX = ')]}\',\n' class SecurityError(Exception): pass class _HandlerMeta(abc.ABCMeta): """Metaclass for our secure base handlers. When a class with this metaclass is defined, the fields are checked to ensure that certain methods we would like to approximate as 'final' are not declared in subclasses. This is because we provide a default implementation which enforces various security related functionality. Class names that can bypass this whitelist are listed in _RESTRICTED_FUNCTION_CLASS_WHITELIST. Restricted methods are listed in _RESTRICTED_FUNCTION_LIST. """ def __new__(mcs, name, bases, dct): if name not in _RESTRICTED_FUNCTION_CLASS_WHITELIST: for func in _RESTRICTED_FUNCTION_LIST: if func in dct: raise SecurityError('%s attempts to override restricted method %s' % (name, func)) return super(_HandlerMeta, mcs).__new__(mcs, name, bases, dct) class BaseHandler(webapp2.RequestHandler): """Base handler for servicing unauthenticated user requests.""" __metaclass__ = _HandlerMeta def __init__(self, request, response): self.initialize(request, response) api_fixer.ReplaceDefaultArgument(response.set_cookie.im_func, 'secure', not constants.IS_DEV_APPSERVER) api_fixer.ReplaceDefaultArgument(response.set_cookie.im_func, 'httponly', True) if self.current_user: self._xsrf_token = xsrf.GenerateToken(_GetXsrfKey(), self.current_user.email()) if self.app.config.get('using_angular', constants.DEFAULT_ANGULAR): # AngularJS requires a JS readable XSRF-TOKEN cookie and will pass this # back in AJAX requests. self.response.set_cookie('XSRF-TOKEN', self._xsrf_token, httponly=False) else: self._xsrf_token = None self.csp_nonce = _GetCspNonce() self._RawWrite = self.response.out.write self.response.out.write = self._ReplacementWrite # All content should be rendered through a template system to reduce the # risk/likelihood of XSS issues. Access to the original function # self.response.out.write is available via self._RawWrite for exceptional # circumstances. def _ReplacementWrite(*args, **kwargs): raise SecurityError('All response content must originate via render() or' 'render_json()') def _SetCommonResponseHeaders(self): """Sets various headers with security implications.""" frame_policy = self.app.config.get('framing_policy', constants.DENY) frame_header_value = constants.X_FRAME_OPTIONS_VALUES.get(frame_policy, '') if frame_header_value: self.response.headers['X-Frame-Options'] = frame_header_value hsts_policy = self.app.config.get('hsts_policy', constants.DEFAULT_HSTS_POLICY) if self.request.scheme.lower() == 'https' and hsts_policy: include_subdomains = bool(hsts_policy.get('includeSubdomains', False)) subdomain_string = '; includeSubdomains' if include_subdomains else '' hsts_value = 'max-age=%d%s' % (int(hsts_policy.get('max_age')), subdomain_string) self.response.headers['Strict-Transport-Security'] = hsts_value self.response.headers['X-XSS-Protection'] = '1; mode=block' self.response.headers['X-Content-Type-Options'] = 'nosniff' csp_policy = self.app.config.get('csp_policy', constants.DEFAULT_CSP_POLICY) report_only = False if 'reportOnly' in csp_policy: report_only = csp_policy.get('reportOnly') csp_policy = csp_policy.copy() del csp_policy['reportOnly'] header_name = ('Content-Security-Policy%s' % ('-Report-Only' if report_only else '')) directives = [] for (k, v) in csp_policy.iteritems(): directives.append('%s %s' % (k, v)) csp = '; '.join(directives) # Set random nonce per response csp = csp % {'nonce_value': self.csp_nonce} self.response.headers.add(header_name, csp) @webapp2.cached_property def current_user(self): return users.get_current_user() def dispatch(self): self._SetCommonResponseHeaders() super(BaseHandler, self).dispatch() @classmethod def get_jinja2_config(cls): """ Builds Jinja2 config based on constants. Note: this is used in the factory below, but an alternative way of setting up Jinja2 would be to use the WSGIApplication config to set this and not use the factory below. This has the advantage of having different settings for different applications and not set here at the handler level. """ extensions = ['jinja2.ext.with_'] return { 'environment_args': { 'autoescape': True, 'extensions': extensions, 'auto_reload': constants.DEBUG, }, 'template_path': constants.TEMPLATE_DIR } @staticmethod def j2_factory(app): """ The factory function passed to get_jinja2. Args: app: the WSGIApplication """ return jinja2.Jinja2(app, BaseHandler.get_jinja2_config()) @webapp2.cached_property def jinja2(self): """ Get the cached Jinja2 instance from the app registry, if none exists the factory function is used to create one. """ return jinja2.get_jinja2(self.j2_factory, app=self.app) def render_to_string(self, template, template_values=None): """Renders template_name with template_values and returns as a string.""" if not template_values: template_values = {} template_values['_xsrf'] = self._xsrf_token template_values['_csp_nonce'] = self.csp_nonce template_strategy = self.app.config.get('template', constants.CLOSURE) if template_strategy == constants.DJANGO: t = django.template.loader.get_template(template) template_values = django.template.Context(template_values) return t.render(template_values) elif template_strategy == constants.JINJA2: return self.jinja2.render_template(template, **template_values) else: ijdata = { 'csp_nonce': self.csp_nonce } return template(template_values, ijdata) def render(self, template, template_values=None): """Renders template with template_values and writes to the response.""" template_strategy = self.app.config.get('template', constants.CLOSURE) self._RawWrite(self.render_to_string(template, template_values)) class BaseCronHandler(BaseHandler): """Base handler for servicing Cron requests. This handler enforces that inbound requests contain the X-AppEngine-Cron header, which AppEngine guarantees is only present on actual invocations according to the cron schedule, or crafted requests by an administrator of the application (the header is filtered out from normal user requests). """ __metaclass__ = _HandlerMeta def dispatch(self): header = self.request.headers.get('X-AppEngine-Cron', 'false') if header != 'true': raise SecurityError('attempt to access cron handler without ' 'X-AppEngine-Cron header') super(BaseCronHandler, self).dispatch() class BaseTaskHandler(BaseHandler): """Base handler for servicing task requests. This handler enforces that inbound requests contain the X-AppEngine-QueueName header, which AppEngine guarantees is only present on requests from the Task Queue API, or crafted requests by an administrator of the application (the header is filtered out from normal user requests). """ __metaclass__ = _HandlerMeta def dispatch(self): header = self.request.headers.get('X-AppEngine-QueueName', None) if not header: raise SecurityError('attempt to access task handler without ' 'X-AppEngine-QueueName header') super(BaseTaskHandler, self).dispatch() class BaseAjaxHandler(BaseHandler): """Base handler for servicing unauthenticated AJAX requests. Responses to GET requests will be prefixed by _XSSI_PREFIX. Requests using other HTTP verbs will not include such a prefix. """ __metaclass__ = _HandlerMeta def _SetAjaxResponseHeaders(self): self.response.headers['Content-Disposition'] = 'attachment; filename=json' self.response.headers['Content-Type'] = 'application/json; charset=utf-8' def dispatch(self): self._SetAjaxResponseHeaders() if self.request.method.lower() == 'get': self._RawWrite(_XSSI_PREFIX) super(BaseAjaxHandler, self).dispatch() def render(self, *args, **kwargs): raise SecurityError('AJAX handlers must use render_json()') def render_json(self, obj): self._RawWrite(json.dumps(obj)) class AuthenticatedHandler(BaseHandler): """Base handler for servicing authenticated user requests. Implementations should provide an implementation of DenyAccess() and XsrfFail() to handle unauthenticated requests or invalid XSRF tokens. POST requests will be rejected unless the request contains a parameter named 'xsrf' which is a valid XSRF token for the currently authenticated user. """ __metaclass__ = _HandlerMeta @requires_auth @xsrf_protected def dispatch(self): super(AuthenticatedHandler, self).dispatch() def _RequestContainsValidXsrfToken(self): token = self.request.get('xsrf') or self.request.headers.get('X-XSRF-TOKEN') # By default, Angular's $http service will add quotes around the # X-XSRF-TOKEN. if (token and self.app.config.get('using_angular', constants.DEFAULT_ANGULAR) and token[0] == '"' and token[-1] == '"'): token = token[1:-1] if xsrf.ValidateToken(_GetXsrfKey(), self.current_user.email(), token): return True return False @abc.abstractmethod def DenyAccess(self): pass @abc.abstractmethod def XsrfFail(self): pass class AuthenticatedAjaxHandler(BaseAjaxHandler): """Base handler for servicing AJAX requests. Implementations should provide an implementation of DenyAccess() and XsrfFail() to handle unauthenticated requests or invalid XSRF tokens. POST requests will be rejected unless the request contains a parameter named 'xsrf', OR an HTTP header named 'X-XSRF-Token' which is a valid XSRF token for the currently authenticated user. Responses to GET requests will be prefixed by _XSSI_PREFIX. Requests using other HTTP verbs will not include such a prefix. """ __metaclass__ = _HandlerMeta @requires_auth @xsrf_protected def dispatch(self): super(AuthenticatedAjaxHandler, self).dispatch() def _RequestContainsValidXsrfToken(self): token = self.request.get('xsrf') or self.request.headers.get('X-XSRF-Token') # By default, Angular's $http service will add quotes around the # X-XSRF-TOKEN. if (token and self.app.config.get('using_angular', constants.DEFAULT_ANGULAR) and token[0] == '"' and token[-1] == '"'): token = token[1:-1] if xsrf.ValidateToken(_GetXsrfKey(), self.current_user.email(), token): return True return False @abc.abstractmethod def DenyAccess(self): pass @abc.abstractmethod def XsrfFail(self): pass class AdminHandler(AuthenticatedHandler): """Base handler for servicing administrator requests. Implementations should provide an implementation of DenyAccess() and XsrfFail() to handle unauthenticated requests or invalid XSRF tokens. Requests will be rejected if the currently logged in user is not an administrator. POST requests will be rejected unless the request contains a parameter named 'xsrf' which is a valid XSRF token for the currently authenticated user. """ __metaclass__ = _HandlerMeta @requires_admin def dispatch(self): super(AdminHandler, self).dispatch() class AdminAjaxHandler(AuthenticatedAjaxHandler): """Base handler for servicing AJAX administrator requests. Implementations should provide an implementation of DenyAccess() and XsrfFail() to handle unauthenticated requests or invalid XSRF tokens. Requests will be rejected if the currently logged in user is not an administrator. POST requests will be rejected unless the request contains a parameter named 'xsrf', OR an HTTP header named 'X-XSRF-Token' which is a valid XSRF token for the currently authenticated user. Responses to GET requests will be prefixed by _XSSI_PREFIX. Requests using other HTTP verbs will not include such a prefix. """ __metaclass__ = _HandlerMeta @requires_admin def dispatch(self): super(AdminAjaxHandler, self).dispatch() ================================================ FILE: python/base/handlers_test.py ================================================ # Copyright 2014 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Tests for base.handlers.""" import exceptions import unittest2 import webapp2 import handlers import xsrf from google.appengine.ext import testbed class DummyHandler(handlers.AuthenticatedHandler): """Convenience class to verify successful requests.""" def get(self): self._RawWrite('get_succeeded') def post(self): self._RawWrite('post_succeeded') def DenyAccess(self): self._RawWrite('access_denied') def XsrfFail(self): self._RawWrite('xsrf_fail') class DummyAjaxHandler(handlers.BaseAjaxHandler): """Convenience class to verify successful requests.""" def get(self): pass def post(self): pass class DummyCronHandler(handlers.BaseCronHandler): """Convenience class to verify successful requests.""" def get(self): self._RawWrite('get_succeeded') class DummyTaskHandler(handlers.BaseTaskHandler): """Convenience class to verify successful requests.""" def get(self): self._RawWrite('get_succeeded') class HandlersTest(unittest2.TestCase): """Test cases for base.handlers.""" def setUp(self): self.testbed = testbed.Testbed() self.testbed.activate() self.testbed.init_datastore_v3_stub() self.testbed.init_memcache_stub() self.app = webapp2.WSGIApplication([('/', DummyHandler), ('/ajax', DummyAjaxHandler), ('/cron', DummyCronHandler), ('/task', DummyTaskHandler)]) def _FakeLogin(self): """Sets up the environment to have a fake user logged in.""" self.testbed.setup_env( USER_EMAIL='user@example.com', USER_ID='123', overwrite=True) def testHandlerCannotOverrideFinalMethods(self): try: class _(handlers.BaseHandler): def dispatch(self): pass self.fail('should not be able to override dispatch') except handlers.SecurityError, e: self.assertTrue(e.message.find('override restricted') != -1) def testAuthenticatedHandlerRequiresUser(self): self.assertEqual('access_denied', self.app.get_response('/').body) self.assertEqual('access_denied', self.app.get_response('/', method='POST').body) self._FakeLogin() self.assertEqual('get_succeeded', self.app.get_response('/').body) def testXsrfProtectionFailsWithInvalidToken(self): self._FakeLogin() self.assertEqual('xsrf_fail', self.app.get_response('/', method='POST', POST={}).body) def testXsrfProtectionSucceedsWithValidToken(self): self._FakeLogin() key = handlers._GetXsrfKey() token = xsrf.GenerateToken(key, 'user@example.com') self.assertEqual('post_succeeded', self.app.get_response('/', method='POST', POST={'xsrf': token}).body) def testResponseHasStrictCSP(self): """Checks that the CSP in the response is set and strict. More information: https://www.w3.org/TR/CSP3/#strict-dynamic-usage """ fakeNonce = 'rand0m123' strictScriptSrc = ['\'strict-dynamic\'', '\'nonce-%s\'' % fakeNonce] strictObjectSrc = ['\'none\''] handlers._GetCspNonce = lambda : fakeNonce headers = self.app.get_response('/', method='GET').headers csp_header = headers.get('Content-Security-Policy') self.assertIsNotNone(csp_header) csp = {x.split()[0]: x.split()[1:] for x in csp_header.split(';')} # Check that csp contains a nonce and the stict-dynamic keyword. self.assertTrue(set(strictScriptSrc) <= set(csp.get('script-src'))) self.assertListEqual(strictObjectSrc, csp.get('object-src')) def testAjaxGetResponsesIncludeXssiPrefix(self): self.assertEqual(handlers._XSSI_PREFIX, self.app.get_response('/ajax').body) def testAjaxPostResponsesLackXssiPrefix(self): self.assertEqual('', self.app.get_response('/ajax', method='POST').body) def testCronFailsWithoutXAppEngineCron(self): try: self.app.get_response('/cron', method='GET') self.fail('Cron succeeded without X-AppEngine-Cron: true header') except exceptions.AssertionError, e: # webapp2 wraps the raised SecurityError during dispatch with an # exceptions.AssertionError. self.assertTrue(e.message.find('X-AppEngine-Cron') != -1) def testCronSucceedsWithXAppEngineCron(self): headers = [('X-AppEngine-Cron', 'true')] self.assertEqual('get_succeeded', self.app.get_response('/cron', headers=headers).body) def testTaskFailsWithoutXAppEngineQueueName(self): try: self.app.get_response('/task', method='GET') self.fail('Task succeeded without X-AppEngine-QueueName header') except exceptions.AssertionError, e: # webapp2 wraps the raised SecurityError during dispatch with an # exceptions.AssertionError. self.assertTrue(e.message.find('X-AppEngine-QueueName') != -1) def testTaskSucceedsWithXAppEngineQueueName(self): headers = [('X-AppEngine-QueueName', 'default')] self.assertEqual('get_succeeded', self.app.get_response('/task', headers=headers).body) if __name__ == '__main__': unittest2.main() ================================================ FILE: python/base/models.py ================================================ # Copyright 2014 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Framework wide datastore models.""" from google.appengine.ext import ndb import os @ndb.transactional def GetApplicationConfiguration(): """Returns the application configuration, creating it if necessary.""" key = ndb.Key(Config, 'config') entity = key.get() if not entity: entity = Config(key=key) entity.xsrf_key = os.urandom(16) entity.put() return entity class Config(ndb.Model): """A simple key-value store for application configuration settings.""" xsrf_key = ndb.BlobProperty() ================================================ FILE: python/base/models_test.py ================================================ # Copyright 2014 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Tests for base.models.""" import unittest2 import models from google.appengine.ext import testbed class ModelsTest(unittest2.TestCase): """Test cases for base.models.""" def setUp(self): self.testbed = testbed.Testbed() self.testbed.activate() self.testbed.init_datastore_v3_stub() def testConfigurationAutomaticallyGenerated(self): config = models.GetApplicationConfiguration() self.assertIsNotNone(config) self.assertIsNotNone(config.xsrf_key) if __name__ == '__main__': unittest2.main() ================================================ FILE: python/base/xsrf.py ================================================ # Copyright 2014 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Utilities related to Cross-Site Request Forgery protection.""" import hashlib import hmac import time DELIMITER_ = ':' DEFAULT_TIMEOUT_ = 86400 def _Compare(a, b): """Compares a and b in constant time and returns True if they are equal.""" if len(a) != len(b): return False result = 0 for x, y in zip(a, b): result |= ord(x) ^ ord(y) return result == 0 def GenerateToken(key, user, action='*', now=None): """Generates an XSRF token for the provided user and action.""" token_timestamp = now or int(time.time()) message = DELIMITER_.join([user, action, str(token_timestamp)]) digest = hmac.new(key, message, hashlib.sha1).hexdigest() return DELIMITER_.join([str(token_timestamp), digest]) def ValidateToken(key, user, token, action='*', max_age=DEFAULT_TIMEOUT_): """Validates the provided XSRF token.""" if not token or not user: return False try: (timestamp, digest) = token.split(DELIMITER_) except ValueError: return False expected = GenerateToken(key, user, action, timestamp) (_, expected_digest) = expected.split(DELIMITER_) now = int(time.time()) if _Compare(expected_digest, digest) and now < int(timestamp) + max_age: return True return False ================================================ FILE: python/base/xsrf_test.py ================================================ # Copyright 2014 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Tests for base.xsrf.""" import os import time import unittest2 import xsrf class XsrfTest(unittest2.TestCase): """Test cases for base.xsrf.""" def setUp(self): # non-deterministic tests FTW! self.key = os.urandom(16) def testCompare(self): self.assertTrue(xsrf._Compare('a', 'a')) self.assertFalse(xsrf._Compare('a', 'b')) self.assertFalse(xsrf._Compare('a', 'ab')) def testTokenWithNoActionVerifies(self): token = xsrf.GenerateToken(self.key, 'user') self.assertTrue(xsrf.ValidateToken(self.key, 'user', token)) def testTokenWithDifferentActionsFail(self): token = xsrf.GenerateToken(self.key, 'user', 'a') self.assertFalse(xsrf.ValidateToken(self.key, 'user', token, 'b')) def testTokenWithDifferentUsersFail(self): token = xsrf.GenerateToken(self.key, 'user') self.assertFalse(xsrf.ValidateToken(self.key, 'otheruser', token)) def testExpiredTokenDoesNotVerify(self): now = int(time.time()) - (xsrf.DEFAULT_TIMEOUT_ + 1) token = xsrf.GenerateToken(self.key, 'user', '*', now) self.assertFalse(xsrf.ValidateToken(self.key, 'user', token)) self.assertTrue(xsrf.ValidateToken(self.key, 'user', token, '*', xsrf.DEFAULT_TIMEOUT_ * 2)) if __name__ == '__main__': unittest2.main() ================================================ FILE: python/country_servers.py ================================================ # Copyright 2017 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys from random import randint from google.appengine.ext import ndb import logging # the top level of your domain in which you'll run backend servers, e.g. your-domain.com domain = '' # regions you might have treehouse servers in. # each of these must be running an instance of the app in 'backend' # with DNS set up appropriately (e.g. forest-rooms-us.your-domain.com). us = 'us' asia = 'asia' europe = 'europe' # next we get the list of countries mapped to server they should use, # if there's a regional server for that region. country list is # derived from http://dev.maxmind.com/static/csv/codes/country_continent.csv # & mapped to us/asia/europe thus: # NA -> us # SA -> us # EU -> eu # AF -> eu # AS -> as # OC -> as # AN -> us country_to_server_map = { 'A1': us, 'A2': us, 'AD': europe, 'AE': asia, 'AF': asia, 'AG': us, 'AI': us, 'AL': europe, 'AM': asia, 'AN': us, 'AO': europe, 'AP': asia, 'AQ': us, 'AR': us, 'AS': asia, 'AT': europe, 'AU': asia, 'AW': us, 'AX': europe, 'AZ': asia, 'BA': europe, 'BB': us, 'BD': asia, 'BE': europe, 'BF': europe, 'BG': europe, 'BH': asia, 'BI': europe, 'BJ': europe, 'BL': us, 'BM': us, 'BN': asia, 'BO': us, 'BR': us, 'BS': us, 'BT': asia, 'BV': us, 'BW': europe, 'BY': europe, 'BZ': us, 'CA': us, 'CC': asia, 'CD': europe, 'CF': europe, 'CG': europe, 'CH': europe, 'CI': europe, 'CK': asia, 'CL': us, 'CM': europe, 'CN': asia, 'CO': us, 'CR': us, 'CU': us, 'CV': europe, 'CX': asia, 'CY': asia, 'CZ': europe, 'DE': europe, 'DJ': europe, 'DK': europe, 'DM': us, 'DO': us, 'DZ': europe, 'EC': us, 'EE': europe, 'EG': europe, 'EH': europe, 'ER': europe, 'ES': europe, 'ET': europe, 'EU': europe, 'FI': europe, 'FJ': asia, 'FK': us, 'FM': asia, 'FO': europe, 'FR': europe, 'FX': europe, 'GA': europe, 'GB': europe, 'GD': us, 'GE': asia, 'GF': us, 'GG': europe, 'GH': europe, 'GI': europe, 'GL': us, 'GM': europe, 'GN': europe, 'GP': us, 'GQ': europe, 'GR': europe, 'GS': us, 'GT': us, 'GU': asia, 'GW': europe, 'GY': us, 'HK': asia, 'HM': us, 'HN': us, 'HR': europe, 'HT': us, 'HU': europe, 'ID': asia, 'IE': europe, 'IL': asia, 'IM': europe, 'IN': asia, 'IO': asia, 'IQ': asia, 'IR': asia, 'IS': europe, 'IT': europe, 'JE': europe, 'JM': us, 'JO': asia, 'JP': asia, 'KE': europe, 'KG': asia, 'KH': asia, 'KI': asia, 'KM': europe, 'KN': us, 'KP': asia, 'KR': asia, 'KW': asia, 'KY': us, 'KZ': asia, 'LA': asia, 'LB': asia, 'LC': us, 'LI': europe, 'LK': asia, 'LR': europe, 'LS': europe, 'LT': europe, 'LU': europe, 'LV': europe, 'LY': europe, 'MA': europe, 'MC': europe, 'MD': europe, 'ME': europe, 'MF': us, 'MG': europe, 'MH': asia, 'MK': europe, 'ML': europe, 'MM': asia, 'MN': asia, 'MO': asia, 'MP': asia, 'MQ': us, 'MR': europe, 'MS': us, 'MT': europe, 'MU': europe, 'MV': asia, 'MW': europe, 'MX': us, 'MY': asia, 'MZ': europe, 'NA': europe, 'NC': asia, 'NE': europe, 'NF': asia, 'NG': europe, 'NI': us, 'NL': europe, 'NO': europe, 'NP': asia, 'NR': asia, 'NU': asia, 'NZ': asia, 'O1': us, 'OM': asia, 'PA': us, 'PE': us, 'PF': asia, 'PG': asia, 'PH': asia, 'PK': asia, 'PL': europe, 'PM': us, 'PN': asia, 'PR': us, 'PS': asia, 'PT': europe, 'PW': asia, 'PY': us, 'QA': asia, 'RE': europe, 'RO': europe, 'RS': europe, 'RU': europe, 'RW': europe, 'SA': asia, 'SB': asia, 'SC': europe, 'SD': europe, 'SE': europe, 'SG': asia, 'SH': europe, 'SI': europe, 'SJ': europe, 'SK': europe, 'SL': europe, 'SM': europe, 'SN': europe, 'SO': europe, 'SR': us, 'ST': europe, 'SV': us, 'SY': asia, 'SZ': europe, 'TC': us, 'TD': europe, 'TF': us, 'TG': europe, 'TH': asia, 'TJ': asia, 'TK': asia, 'TL': asia, 'TM': asia, 'TN': europe, 'TO': asia, 'TR': europe, 'TT': us, 'TV': asia, 'TW': asia, 'TZ': europe, 'UA': europe, 'UG': europe, 'UM': asia, 'US': us, 'UY': us, 'UZ': asia, 'VA': europe, 'VC': us, 'VE': us, 'VG': us, 'VI': us, 'VN': asia, 'VU': asia, 'WF': asia, 'WS': asia, 'YE': asia, 'YT': europe, 'ZA': europe, 'ZM': europe, 'ZW': europe, 'ZZ': us } def get_region_for_country( client_country ): region = country_to_server_map[client_country.upper()] if region is None: region = 'us' return region def get_all_servers(): servers = RegionalRoomServer.query().fetch(10) if servers is None or len(servers) == 0: servers = [] server = RegionalRoomServer() server.name = 'us' # you'll need to modify this appropriately for your deployment setup server.hostname = 'forest-rooms-' + server.name + '.' + domain server.put() servers.append(server) logging.info("Auto populated datastore") return servers class RegionalRoomServer(ndb.Model): name = ndb.StringProperty('name', indexed=True) hostname = ndb.StringProperty('hostname', indexed=True) if __name__ == "__main__": if len( sys.argv ) == 2: print get_server_for_country( sys.argv[ 1 ] ) ================================================ FILE: python/handlers.py ================================================ # Copyright 2014 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import logging import country_servers from base import handlers # Minimal set of handlers to let you display main page with examples class RootHandler(handlers.BaseHandler): def get(self): self.render('index.html') class ConfigHandler(handlers.BaseHandler): def get(self): servers = country_servers.get_all_servers() country = self.request.headers.get("X-AppEngine-Country") region = country_servers.get_region_for_country(country) self.response.headers['Content-Type'] = 'application/javascript; charset=utf-8' self.render('config.template', { 'default_region': region, 'servers': servers }) class CspHandler(handlers.BaseAjaxHandler): def post(self): try: report = json.loads(self.request.body) logging.warn('CSP Violation: %s' % (json.dumps(report['csp-report']))) self.render_json({}) except: self.render_json({'error': 'invalid CSP report'}) ================================================ FILE: python/main.py ================================================ # Copyright 2014 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Main application entry point.""" import base.api_fixer import webapp2 import base import base.constants import handlers # These should all inherit from base.handlers.BaseHandler _UNAUTHENTICATED_ROUTES = [ ('/', handlers.RootHandler), ('/config.js', handlers.ConfigHandler) ] # These should all inherit from base.handlers.BaseAjaxHandler _UNAUTHENTICATED_AJAX_ROUTES = [('/csp', handlers.CspHandler)] # These should all inherit from base.handlers.AuthenticatedHandler _USER_ROUTES = [] # These should all inherit from base.handlers.AuthenticatedAjaxHandler _AJAX_ROUTES = [] # These should all inherit from base.handlers.AdminHandler _ADMIN_ROUTES = [] # These should all inherit from base.handlers.AdminAjaxHandler _ADMIN_AJAX_ROUTES = [] # These should all inherit from base.handlers.BaseCronHandler _CRON_ROUTES = [] # These should all inherit from base.handlers.BaseTaskHandler _TASK_ROUTES = [] # Place global application configuration settings (e.g. settings for # 'webapp2_extras.sessions') here. # # These values will be accessible from handler methods like this: # self.app.config.get('foo') # # Framework level settings: # template: one of base.constants.CLOSURE (default), base.constants.DJANGO, # or base.constants.JINJA. # # using_angular: True or False (default). When True, an XSRF-TOKEN cookie # will be set for interception/use by Angular's $http service. # When False, no header will be set (but an XSRF token will # still be available under the _xsrf key for Django/Jinja # templates). If you set this to True, be especially careful # when mixing Angular and any server side templates: # https://github.com/angular/angular.js/issues/5601 # See the summary by IgorMinar for details. # # framing_policy: one of base.constants.DENY (default), # base.constants.SAMEORIGIN, or base.constants.PERMIT # # hsts_policy: A dictionary with minimally a 'max_age' key, and optionally # a 'includeSubdomains' boolean member. # Default: { 'max_age': 2592000, 'includeSubDomains': True } # implying 30 days of strict HTTPS for all subdomains. # # csp_policy: A dictionary with keys that correspond to valid CSP # directives, as defined in the W3C CSP 3 spec. Each # key/value pair is transmitted as a distinct # Content-Security-Policy header. # Default: {'default-src': '\'self\''} # which is a very restrictive policy. An optional # 'reportOnly' boolean key substitutes a # 'Content-Security-Policy-Report-Only' header # name in lieu of 'Content-Security-Policy' (the default # is base.constants.DEBUG). # # Note that the default values are also configured in app.yaml for files # served via the /static/ resources. You may need to change the settings # there as well. _CONFIG = { 'template': base.constants.JINJA2, # Developers are encouraged to build sites that comply with this CSP policy. # Changing the first two entries (nonce, strict-dynamic) of the script-src # directive may render XSS protection invalid! For more information take a # look here https://www.w3.org/TR/CSP3/#strict-dynamic-usage # With this policy, modern browsers will execute only those scripts whose # nonce attribute matches the value set in the policy header, as well as # scripts dynamically added to the page by scripts with the proper nonce. # Older browsers, which don't support the CSP3 standard, will ignore the # nonce-* and 'strict-dynamic' keywords and fall back to [script-src # 'unsafe-inline' https: http:] which will not provide protection against # XSS vulnerabilities, but will allow the application to function properly. 'csp_policy': { # Disallow Flash, etc. 'object-src': '\'none\'', # Strict CSP with fallbacks for browsers not supporting CSP v3. 'script-src': # Propagate trust to dynamically created scripts. # '\'strict-dynamic\' ' # Fallback. Ignored in presence of a nonce # '\'unsafe-inline\' ' '\'unsafe-eval\' ' # Fallback. Ignored in presence of strict-dynamic. 'https: http:', 'report-uri': '/csp', 'reportOnly': base.constants.DEBUG, } } ################################# # DO NOT MODIFY BELOW THIS LINE # ################################# app = webapp2.WSGIApplication( routes=(_UNAUTHENTICATED_ROUTES + _UNAUTHENTICATED_AJAX_ROUTES + _USER_ROUTES + _AJAX_ROUTES + _ADMIN_ROUTES + _ADMIN_AJAX_ROUTES + _CRON_ROUTES + _TASK_ROUTES), debug=base.constants.DEBUG, config=_CONFIG) ================================================ FILE: python/main_test.py ================================================ # Copyright 2015 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Tests for main.""" import unittest2 import webapp2 import webapp2_extras.routes from base import handlers import main class MainTest(unittest2.TestCase): """Test cases for main.""" def _VerifyInheritance(self, routes_list, base_class): """Checks that the handlers of the given routes inherit from base_class.""" router = webapp2.Router(routes_list) routes = router.match_routes + router.build_routes.values() inheritance_errors = '' for route in routes: if issubclass(route.__class__, webapp2_extras.routes.MultiRoute): self._VerifyInheritance(list(route.get_routes()), base_class) continue if issubclass(route.handler, webapp2.RedirectHandler): continue if not issubclass(route.handler, base_class): inheritance_errors += '* %s does not inherit from %s.\n' % ( route.handler.__name__, base_class.__name__) return inheritance_errors def testRoutesInheritance(self): errors = '' errors += self._VerifyInheritance(main._UNAUTHENTICATED_ROUTES, handlers.BaseHandler) errors += self._VerifyInheritance(main._UNAUTHENTICATED_AJAX_ROUTES, handlers.BaseAjaxHandler) errors += self._VerifyInheritance(main._USER_ROUTES, handlers.AuthenticatedHandler) errors += self._VerifyInheritance(main._AJAX_ROUTES, handlers.AuthenticatedAjaxHandler) errors += self._VerifyInheritance(main._ADMIN_ROUTES, handlers.AdminHandler) errors += self._VerifyInheritance(main._ADMIN_AJAX_ROUTES, handlers.AdminAjaxHandler) errors += self._VerifyInheritance(main._CRON_ROUTES, handlers.BaseCronHandler) errors += self._VerifyInheritance(main._TASK_ROUTES, handlers.BaseTaskHandler) if errors: self.fail('Some handlers do not inherit from the correct classes:\n' + errors) def testStrictHandlerMethodRouting(self): """Checks that handler functions properly limit applicable HTTP methods.""" router = webapp2.Router(main._USER_ROUTES + main._AJAX_ROUTES + main._ADMIN_ROUTES + main._ADMIN_AJAX_ROUTES) routes = router.match_routes + router.build_routes.values() failed_routes = [] while routes: route = routes.pop() if issubclass(route.__class__, webapp2_extras.routes.MultiRoute): routes += list(route.get_routes()) continue if issubclass(route.handler, webapp2.RedirectHandler): continue if route.handler_method and not route.methods: failed_routes.append('%s (%s)' % (route.template, route.handler.__name__)) if failed_routes: self.fail('Some handlers specify a handler_method but are missing a ' 'methods" attribute and may be vulnerable to XSRF via GET ' 'requests:\n * ' + '\n * '.join(failed_routes)) if __name__ == '__main__': unittest2.main() ================================================ FILE: static/models/bg-tree-1.obj ================================================ # WaveFront *.obj file (generated by CINEMA 4D) g T0 usemtl default v 0.002211 -50319.394575 643.569099 v -0.001393 50319.394575 643.569099 v 0.001393 -50319.394575 -643.569099 v -0.002211 50319.394575 -643.569099 v 193.430643 34224.063607 -1187.421986 v 193.430639 34438.054141 -1187.421986 v 383.49713 34461.72185 -1693.550947 v 344.742168 34614.596245 -1548.91547 v 610.230923 36066.252359 -1633.227163 v 586.296569 36078.629597 -1543.902939 v 671.454181 37751.454252 -1590.129769 v 651.708339 37761.665474 -1516.437285 v -2.54236 34456.199533 -534.75604 v -2.542356 34205.918206 -534.756041 v 0.775234 -10454.994775 644.508516 v 0.775239 -10787.90725 644.508516 v 408.012358 -8811.710688 1230.435558 v 119.402512 -10371.22938 1145.224187 v 154.973641 -10132.832403 1239.839748 v 138.473894 -10126.859301 1123.71217 v 403.015706 -8809.901841 1195.268411 v 119.402511 -10262.968225 990.152164 v -187.00359 -20452.489717 1025.925562 v -321.913333 -20266.132184 1354.251253 v 0.277238 -21002.230843 616.150247 v 0.277232 -20551.211217 616.150247 v -509.5859 -19134.36006 1252.95692 v -118.795314 -20846.756346 838.935504 v -294.866232 -20268.006517 1232.553183 v -544.817164 -19139.754641 1245.209942 v -118.795319 -20517.963039 838.935504 v -222.259557 -20576.323141 1272.491589 v -500.389886 -19134.997333 1211.579576 v -535.62115 -19140.391914 1203.832598 v -4.748229 -35836.423107 -635.553579 v -4.748226 -36061.698312 -635.553579 v -37.70897 -35797.977389 -671.28529 v -66.070073 -35941.624083 -777.130376 v -57.67723 -35712.621843 -698.78891 v -98.170648 -35720.272713 -849.912402 v -118.930904 -33413.896828 -753.777431 v -136.487838 -33417.214054 -819.300799 v -2.397508 10982.775385 624.427512 v -2.397504 10701.522601 624.427511 v -89.939652 10938.618696 780.680051 v -225.4429 10800.184667 1075.97653 v -89.93965 10745.679286 780.680051 v -225.442901 10884.11331 1075.97653 vt 0 -1 0 vt 0 0 0 vt 1 -1 0 vt 1 0 0 vt 0 -1 0 vt 1 -1 0 vt 0 0 0 vt 1 0 0 vt 1 -1 0 vt 0 -1 0 vt 1 0 0 vt 0 0 0 vt 0 -1 0 vt 1 -1 0 vt 0 0 0 vt 1 0 0 vt 1 -1 0 vt 1 0 0 vt 0 0 0 vt 0 -1 0 vt 0.416667 -0.359412 0 vt 0.416667 -0.36438 0 vt 0.416667 -0.36438 0 vt 0.416667 -0.36438 0 vt 0.416667 -0.36438 0 vt 0.416667 -0.359412 0 vt 0.416667 -0.359412 0 vt 0.416667 -0.359412 0 vt 0.5 -0.529565 0 vt 0.5 -0.538841 0 vt 0.5 -0.538841 0 vt 0.5 -0.529565 0 vt 0.5 -0.538841 0 vt 0.5 -0.538841 0 vt 0.5 -0.529565 0 vt 0.583333 -0.538841 0 vt 0.5 -0.529565 0 vt 0.5 -0.538841 0 vt 0.5 -0.529565 0 vt 0.583333 -0.529565 0 vt 1 -0.127661 0 vt 1 -0.14238 0 vt 1 -0.127661 0 vt 1 -0.14238 0 vt 1 -0.127661 0 vt 1 -0.14238 0 vt 1 -0.127661 0 vt 1 -0.14238 0 vt 1 -0.472594 0 vt 1 -0.449065 0 vt 1 -0.472594 0 vt 1 -0.449065 0 vt 1 -0.449065 0 vt 1 -0.472594 0 f 1/1 2/2 4/4 3/3 f 13/19 6/8 5/6 14/20 f 8/12 10/16 9/14 7/10 f 6/7 8/11 7/9 5/5 f 10/15 12/18 11/17 9/13 f 20/26 21/27 17/23 19/25 f 31/37 23/29 32/38 28/34 f 23/29 29/35 24/30 32/38 f 29/35 33/39 27/33 24/30 f 26/32 31/37 28/34 25/31 f 27/33 33/39 34/40 30/36 f 15/21 22/28 18/24 16/22 f 22/28 20/26 19/25 18/24 f 36/42 38/44 37/43 35/41 f 40/46 42/48 41/47 39/45 f 38/44 40/46 39/45 37/43 f 43/49 45/51 47/53 44/50 f 45/51 48/54 46/52 47/53 ================================================ FILE: static/models/bg-tree-2.obj ================================================ # WaveFront *.obj file (generated by CINEMA 4D) g T1 usemtl Mat v 47.567647 24592.814791 609.948777 v 47.567647 24311.562006 609.948789 v 135.109797 24548.65811 766.201315 v 270.613065 24410.224103 1231.279249 v 135.109797 24355.7187 766.201323 v 270.613065 24494.152746 1231.279246 v 0.001021 -50310.62883 -643.507891 v -0.001021 50328.160319 -643.503932 v 0.001021 -50310.62888 643.504854 v -0.001021 50328.160269 643.508813 v -233.52329 -23555.630286 -1124.031178 v -233.52329 -23341.639752 -1124.031188 v -430.637956 -23317.972071 -1627.457058 v -389.867337 -23165.097668 -1483.376794 v -656.507416 -21713.441563 -1563.973519 v -631.328228 -21701.064319 -1474.99218 v -717.122996 -20028.239668 -1520.025596 v -696.350165 -20018.028442 -1446.615992 v -28.456706 -23573.775651 -474.165076 v -28.456706 -23323.494324 -474.165088 v 23.257412 16226.215385 623.971393 v 23.257412 16559.12786 623.971377 v -88.367203 16924.682334 1126.294535 v -375.7592 18484.201026 1215.527159 v -107.737085 17169.052411 1105.050882 v -122.613825 17163.079315 1221.397514 v -90.532353 17032.943482 971.237623 v -371.254047 18486.009871 1180.293676 v 31.640006 6011.891792 617.811433 v 31.640005 6462.911418 617.811411 v 224.623943 6561.63294 1024.931922 v 364.104687 6747.990491 1351.341962 v 335.361048 6746.116152 1230.033392 v 585.464314 7294.214092 1239.198997 v 550.344649 7299.608673 1247.437126 v 153.811525 6167.366301 838.912449 v 153.811525 6496.159608 838.912434 v 263.319084 6437.799529 1270.98167 v 540.571812 7298.971397 1206.192213 v 575.691477 7293.576817 1197.954083 v -34.871549 -1407.185261 -624.000499 v -34.871549 -1688.438045 -624.000509 v -124.586792 -1451.341942 -779.015526 v -264.199826 -1589.775958 -1072.391301 v -124.586792 -1644.281352 -779.015534 v -264.199826 -1505.847315 -1072.391298 vt 1 -0.472594 0 vt 1 -0.449065 0 vt 1 -0.472594 0 vt 1 -0.449065 0 vt 1 -0.449065 0 vt 1 -0.472594 0 vt 0 -1 0 vt 0 0 0 vt 1 -1 0 vt 1 0 0 vt 0 -1 0 vt 1 -1 0 vt 0 0 0 vt 1 0 0 vt 1 -1 0 vt 0 -1 0 vt 1 0 0 vt 0 0 0 vt 0 -1 0 vt 1 -1 0 vt 0 0 0 vt 1 0 0 vt 1 -1 0 vt 1 0 0 vt 0 -1 0 vt 0 0 0 vt 0.416667 -0.36438 0 vt 0.416667 -0.359412 0 vt 0.416667 -0.36438 0 vt 0.416667 -0.36438 0 vt 0.416667 -0.359412 0 vt 0.416667 -0.36438 0 vt 0.416667 -0.359412 0 vt 0.416667 -0.359412 0 vt 0.5 -0.538841 0 vt 0.5 -0.529565 0 vt 0.5 -0.529565 0 vt 0.5 -0.538841 0 vt 0.5 -0.529565 0 vt 0.583333 -0.538841 0 vt 0.5 -0.538841 0 vt 0.5 -0.538841 0 vt 0.5 -0.529565 0 vt 0.5 -0.538841 0 vt 0.5 -0.529565 0 vt 0.583333 -0.529565 0 vt 1 -0.472594 0 vt 1 -0.449065 0 vt 1 -0.472594 0 vt 1 -0.449065 0 vt 1 -0.449065 0 vt 1 -0.472594 0 f 1/1 3/3 5/5 2/2 f 3/3 6/6 4/4 5/5 f 7/7 8/8 10/10 9/9 f 20/26 12/14 11/12 19/25 f 14/18 16/22 15/20 13/16 f 12/13 14/17 13/15 11/11 f 16/21 18/24 17/23 15/19 f 25/31 28/34 24/30 26/32 f 37/43 31/37 38/44 36/42 f 31/37 33/39 32/38 38/44 f 33/39 39/45 35/41 32/38 f 30/36 37/43 36/42 29/35 f 35/41 39/45 40/46 34/40 f 22/28 27/33 23/29 21/27 f 27/33 25/31 26/32 23/29 f 41/47 43/49 45/51 42/48 f 43/49 46/52 44/50 45/51 ================================================ FILE: static/models/bg-tree-3.obj ================================================ # WaveFront *.obj file (generated by CINEMA 4D) g T2 usemtl default v 0.002266 -50319.395029 643.569333 v -0.001339 50319.39412 643.569333 v 0.001448 -50319.395029 -643.568866 v -0.002157 50319.39412 -643.568866 v 6.222563 39584.048537 624.427373 v 6.222567 39302.795752 624.427369 v -81.319583 39539.891846 780.679911 v -216.822833 39401.457814 1075.976387 v -81.31958 39346.952436 780.679908 v -216.822834 39485.386458 1075.976388 v 6.222475 45584.048538 624.427429 v 6.222479 45302.795754 624.427426 v -216.822919 45401.457817 1075.976444 v -81.31967 45539.891848 780.679967 v -216.82292 45485.386461 1075.976445 v -81.319667 45346.952438 780.679965 v 153.198441 -22678.489724 -1382.503714 v 153.198444 -22892.480258 -1382.503735 v 202.905553 -22501.947584 -1771.222004 v 241.660514 -22654.821965 -1915.857495 v 370.874999 -21859.781153 -1785.926446 v 394.809353 -21872.158383 -1875.250671 v 426.331935 -20906.4002 -1761.128095 v 446.077777 -20916.611415 -1834.820581 v -3.416941 -22660.344413 -538.481203 v -3.416937 -22910.62574 -538.481228 v 8.46738 22661.790515 643.707006 v 8.467387 22210.770889 643.707005 v -286.676089 22944.995214 1260.109941 v -536.627012 23493.09315 1272.766674 v -178.813445 22760.512014 1053.48232 v -313.723191 22946.869546 1381.808011 v -110.605172 22695.038692 866.492263 v -214.069414 22636.67859 1300.048347 v -501.395748 23498.48773 1280.513653 v -110.605167 22366.245385 866.492262 v -190.656988 -42885.162575 1127.18018 v -492.199733 23497.850457 1239.136309 v -527.430998 23492.455877 1231.38933 v -380.723503 -42647.504383 1633.309155 v -190.656991 -42671.172041 1127.180199 v -607.457341 -41042.973876 1572.985505 v -341.96854 -42494.629974 1488.673693 v -668.680637 -40087.426902 1529.888196 v -583.522984 -41030.596629 1483.661283 v 5.316037 -42903.307912 474.51424 v -648.934792 -40077.215673 1456.195713 v 5.316033 -42653.026585 474.514262 v 1.653124 5833.898383 -646.550315 v 1.65312 6166.810857 -646.550286 v -116.974164 6532.365349 -1147.265937 v -405.584055 8091.88404 -1232.477174 v -136.045553 6776.735425 -1125.7539 v -152.5453 6770.762333 -1241.881478 v -116.974166 6640.62649 -992.193906 v -400.587404 8093.692883 -1197.310027 v -2.158684 -11789.229054 -618.193576 v -2.15869 -11338.209428 -618.193537 v 185.12213 -11239.487887 -1027.968843 v 320.031868 -11053.130321 -1356.294518 v 292.984767 -11055.004666 -1234.596448 v 542.935674 -10506.906721 -1247.253134 v 507.704409 -10501.512141 -1255.000112 v 116.913864 -11633.754535 -840.97882 v 116.913859 -11304.961227 -840.978791 v 220.378101 -11363.321288 -1274.534881 v 498.508395 -10502.149418 -1213.622768 v 533.73966 -10507.543998 -1205.87579 v 0.515238 12134.667163 -618.40535 v 0.515243 11853.414378 -618.405373 v 88.057384 12090.51049 -774.657892 v 223.560636 11952.07649 -1069.954383 v 88.057387 11897.57108 -774.657909 v 223.560635 12036.005133 -1069.954376 vt 0 -1 0 vt 0 0 0 vt 1 -1 0 vt 1 0 0 vt 1 -0.472594 0 vt 1 -0.449065 0 vt 1 -0.472594 0 vt 1 -0.449065 0 vt 1 -0.449065 0 vt 1 -0.472594 0 vt 1 -0.472594 0 vt 1 -0.449065 0 vt 1 -0.449065 0 vt 1 -0.472594 0 vt 1 -0.472594 0 vt 1 -0.449065 0 vt 1 -1 0 vt 0 -1 0 vt 1 0 0 vt 0 0 0 vt 0 -1 0 vt 1 -1 0 vt 0 0 0 vt 1 0 0 vt 0 -1 0 vt 1 -1 0 vt 0 0 0 vt 1 0 0 vt 1 -1 0 vt 1 0 0 vt 0 -1 0 vt 0 0 0 vt 0.5 -0.529565 0 vt 0.5 -0.538841 0 vt 0.5 -0.529565 0 vt 0.583333 -0.538841 0 vt 0.5 -0.529565 0 vt 0.5 -0.538841 0 vt 0.5 -0.529565 0 vt 0.5 -0.538841 0 vt 0.5 -0.538841 0 vt 0.5 -0.538841 0 vt 0 -1 0 vt 1 -1 0 vt 0.5 -0.529565 0 vt 0.583333 -0.529565 0 vt 0 -1 0 vt 1 -1 0 vt 0 0 0 vt 1 0 0 vt 1 -1 0 vt 0 -1 0 vt 0 0 0 vt 1 0 0 vt 1 -1 0 vt 1 0 0 vt 0 0 0 vt 0 -1 0 vt 1 0 0 vt 0 0 0 vt 0.416667 -0.36438 0 vt 0.416667 -0.359412 0 vt 0.416667 -0.36438 0 vt 0.416667 -0.36438 0 vt 0.416667 -0.359412 0 vt 0.416667 -0.36438 0 vt 0.416667 -0.359412 0 vt 0.416667 -0.359412 0 vt 0.5 -0.538841 0 vt 0.5 -0.529565 0 vt 0.5 -0.529565 0 vt 0.5 -0.538841 0 vt 0.5 -0.529565 0 vt 0.583333 -0.538841 0 vt 0.5 -0.538841 0 vt 0.5 -0.538841 0 vt 0.5 -0.529565 0 vt 0.5 -0.538841 0 vt 0.5 -0.529565 0 vt 0.583333 -0.529565 0 vt 1 -0.472594 0 vt 1 -0.449065 0 vt 1 -0.472594 0 vt 1 -0.449065 0 vt 1 -0.449065 0 vt 1 -0.472594 0 f 1/1 2/2 4/4 3/3 f 5/5 7/7 9/9 6/6 f 7/7 10/10 8/8 9/9 f 11/11 14/14 16/16 12/12 f 14/14 15/15 13/13 16/16 f 18/20 20/24 19/22 17/18 f 20/23 22/28 21/26 19/21 f 26/32 18/19 17/17 25/31 f 22/27 24/30 23/29 21/25 f 35/41 38/45 39/46 30/36 f 33/39 31/37 34/40 36/42 f 31/37 29/35 32/38 34/40 f 29/35 38/45 35/41 32/38 f 27/33 33/39 36/42 28/34 f 48/60 41/50 37/44 46/58 f 45/57 47/59 44/55 42/52 f 41/49 43/54 40/48 37/43 f 43/53 45/56 42/51 40/47 f 53/65 56/68 52/64 54/66 f 50/62 55/67 51/63 49/61 f 55/67 53/65 54/66 51/63 f 63/75 67/79 68/80 62/74 f 65/77 59/71 66/78 64/76 f 59/71 61/73 60/72 66/78 f 61/73 67/79 63/75 60/72 f 58/70 65/77 64/76 57/69 f 69/81 71/83 73/85 70/82 f 71/83 74/86 72/84 73/85 ================================================ FILE: static/models/bg-tree-ring.obj ================================================ # WaveFront *.obj file (generated by CINEMA 4D) g Cylinder usemtl Mat v 50 0 0 v 50 100 0 v 49.240388 0 -8.682409 v 49.240388 100 -8.682409 v 46.984631 0 -17.101007 v 46.984631 100 -17.101007 v 43.30127 0 -25 v 43.30127 100 -25 v 38.302222 0 -32.13938 v 38.302222 100 -32.13938 v 32.13938 0 -38.302222 v 32.13938 100 -38.302222 v 25 0 -43.30127 v 25 100 -43.30127 v 17.101007 0 -46.984631 v 17.101007 100 -46.984631 v 8.682409 0 -49.240388 v 8.682409 100 -49.240388 v 0 0 -50 v 0 100 -50 v -8.682409 0 -49.240388 v -8.682409 100 -49.240388 v -17.101007 0 -46.984631 v -17.101007 100 -46.984631 v -25 0 -43.30127 v -25 100 -43.30127 v -32.13938 0 -38.302222 v -32.13938 100 -38.302222 v -38.302222 0 -32.13938 v -38.302222 100 -32.13938 v -43.30127 0 -25 v -43.30127 100 -25 v -46.984631 0 -17.101007 v -46.984631 100 -17.101007 v -49.240388 0 -8.682409 v -49.240388 100 -8.682409 v -50 0 0 v -50 100 0 v -49.240388 0 8.682409 v -49.240388 100 8.682409 v -46.984631 0 17.101007 v -46.984631 100 17.101007 v -43.30127 0 25 v -43.30127 100 25 v -38.302222 0 32.13938 v -38.302222 100 32.13938 v -32.13938 0 38.302222 v -32.13938 100 38.302222 v -25 0 43.30127 v -25 100 43.30127 v -17.101007 0 46.984631 v -17.101007 100 46.984631 v -8.682409 0 49.240388 v -8.682409 100 49.240388 v 0 0 50 v 0 100 50 v 8.682409 0 49.240388 v 8.682409 100 49.240388 v 17.101007 0 46.984631 v 17.101007 100 46.984631 v 25 0 43.30127 v 25 100 43.30127 v 32.13938 0 38.302222 v 32.13938 100 38.302222 v 38.302222 0 32.13938 v 38.302222 100 32.13938 v 43.30127 0 25 v 43.30127 100 25 v 46.984631 0 17.101007 v 46.984631 100 17.101007 v 49.240388 0 8.682409 v 49.240388 100 8.682409 v 102.850059 0 109.0129 v 109.0129 0 102.850059 v 89.497024 0 -120.217427 v 82.357644 0 -125.216475 v 82.357644 0 125.216475 v 89.497024 0 120.217427 v 67.261826 0 -133.932049 v 59.362833 0 -137.61541 v 59.362833 0 137.61541 v 67.261826 0 133.932049 v 42.982912 0 -143.577214 v 34.564313 0 -145.83297 v 34.564313 0 145.83297 v 42.982912 0 143.577214 v 17.397983 0 -148.859857 v 8.715574 0 -149.61947 v 8.715574 0 149.61947 v 17.397983 0 148.859857 v -8.715574 0 -149.61947 v -17.397983 0 -148.859857 v -17.397983 0 148.859857 v -8.715574 0 149.61947 v -34.564313 0 -145.83297 v -42.982912 0 -143.577214 v -42.982912 0 143.577214 v -34.564313 0 145.83297 v -59.362833 0 -137.61541 v -67.261826 0 -133.932049 v -67.261826 0 133.932049 v -59.362833 0 137.61541 v -82.357644 0 -125.216475 v -89.497024 0 -120.217427 v -89.497024 0 120.217427 v -82.357644 0 125.216475 v -102.850059 0 -109.0129 v -109.0129 0 -102.850059 v 149.61947 0 -8.715574 v -109.0129 0 102.850059 v -102.850059 0 109.0129 v 148.859857 0 -17.397983 v 148.859857 0 17.397983 v -120.217427 0 -89.497024 v -125.216475 0 -82.357644 v 149.61947 0 8.715574 v 145.83297 0 -34.564313 v -125.216475 0 82.357644 v -120.217427 0 89.497024 v 143.577214 0 -42.982912 v 143.577214 0 42.982912 v -133.932049 0 -67.261826 v -137.61541 0 -59.362833 v 145.83297 0 34.564313 v 137.61541 0 -59.362833 v -137.61541 0 59.362833 v -133.932049 0 67.261826 v 133.932049 0 -67.261826 v 133.932049 0 67.261826 v -143.577214 0 -42.982912 v -145.83297 0 -34.564313 v 137.61541 0 59.362833 v 125.216475 0 -82.357644 v -145.83297 0 34.564313 v -143.577214 0 42.982912 v 120.217427 0 -89.497024 v 120.217427 0 89.497024 v -148.859857 0 -17.397983 v -149.61947 0 -8.715574 v 125.216475 0 82.357644 v 109.0129 0 -102.850059 v -149.61947 0 8.715574 v -148.859857 0 17.397983 v 102.850059 0 -109.0129 vt 1 0.25 0 vt 0 0.25 0 vt 1 0.5 0 vt 0 0.5 0 vt 1 1 0 vt 0 1 0 vt 0.027778 0.25 0 vt 0.027778 0.5 0 vt 0.027778 1 0 vt 0.055556 0.25 0 vt 0.055556 0.5 0 vt 0.055556 1 0 vt 0.083333 0.25 0 vt 0.083333 0.5 0 vt 0.083333 1 0 vt 0.111111 0.25 0 vt 0.111111 0.5 0 vt 0.111111 1 0 vt 0.138889 0.25 0 vt 0.138889 0.5 0 vt 0.138889 1 0 vt 0.166667 0.25 0 vt 0.166667 0.5 0 vt 0.166667 1 0 vt 0.194444 0.25 0 vt 0.194444 0.5 0 vt 0.194444 1 0 vt 0.222222 0.25 0 vt 0.222222 0.5 0 vt 0.222222 1 0 vt 0.25 0.25 0 vt 0.25 0.5 0 vt 0.25 1 0 vt 0.277778 0.25 0 vt 0.277778 0.5 0 vt 0.277778 1 0 vt 0.305556 0.25 0 vt 0.305556 0.5 0 vt 0.305556 1 0 vt 0.333333 0.25 0 vt 0.333333 0.5 0 vt 0.333333 1 0 vt 0.361111 0.25 0 vt 0.361111 0.5 0 vt 0.361111 1 0 vt 0.388889 0.25 0 vt 0.388889 0.5 0 vt 0.388889 1 0 vt 0.416667 0.25 0 vt 0.416667 0.5 0 vt 0.416667 1 0 vt 0.444444 0.25 0 vt 0.444444 0.5 0 vt 0.444444 1 0 vt 0.472222 0.25 0 vt 0.472222 0.5 0 vt 0.472222 1 0 vt 0.5 0.25 0 vt 0.5 0.5 0 vt 0.5 1 0 vt 0.527778 0.25 0 vt 0.527778 0.5 0 vt 0.527778 1 0 vt 0.555556 0.25 0 vt 0.555556 0.5 0 vt 0.555556 1 0 vt 0.583333 0.25 0 vt 0.583333 0.5 0 vt 0.583333 1 0 vt 0.611111 0.25 0 vt 0.611111 0.5 0 vt 0.611111 1 0 vt 0.638889 0.25 0 vt 0.638889 0.5 0 vt 0.638889 1 0 vt 0.666667 0.25 0 vt 0.666667 0.5 0 vt 0.666667 1 0 vt 0.694444 0.25 0 vt 0.694444 0.5 0 vt 0.694444 1 0 vt 0.722222 0.25 0 vt 0.722222 0.5 0 vt 0.722222 1 0 vt 0.75 0.25 0 vt 0.75 0.5 0 vt 0.75 1 0 vt 0.777778 0.25 0 vt 0.777778 0.5 0 vt 0.777778 1 0 vt 0.805556 0.25 0 vt 0.805556 0.5 0 vt 0.805556 1 0 vt 0.833333 0.25 0 vt 0.833333 0.5 0 vt 0.833333 1 0 vt 0.861111 0.25 0 vt 0.861111 0.5 0 vt 0.861111 1 0 vt 0.888889 0.25 0 vt 0.888889 0.5 0 vt 0.888889 1 0 vt 0.916667 0.25 0 vt 0.916667 0.5 0 vt 0.916667 1 0 vt 0.944444 0.25 0 vt 0.944444 0.5 0 vt 0.944444 1 0 vt 0.972222 0.25 0 vt 0.972222 0.5 0 vt 0.972222 1 0 vt 0.870372 0.25 0 vt 0.879628 0.25 0 vt 0.148149 0.25 0 vt 0.157406 0.25 0 vt 0.842594 0.25 0 vt 0.851851 0.25 0 vt 0.175927 0.25 0 vt 0.185184 0.25 0 vt 0.814816 0.25 0 vt 0.824073 0.25 0 vt 0.203705 0.25 0 vt 0.212962 0.25 0 vt 0.787038 0.25 0 vt 0.796295 0.25 0 vt 0.231483 0.25 0 vt 0.240739 0.25 0 vt 0.759261 0.25 0 vt 0.768517 0.25 0 vt 0.259261 0.25 0 vt 0.268517 0.25 0 vt 0.731483 0.25 0 vt 0.740739 0.25 0 vt 0.287038 0.25 0 vt 0.296295 0.25 0 vt 0.703705 0.25 0 vt 0.712962 0.25 0 vt 0.314816 0.25 0 vt 0.324073 0.25 0 vt 0.675927 0.25 0 vt 0.685184 0.25 0 vt 0.342594 0.25 0 vt 0.351851 0.25 0 vt 0.648149 0.25 0 vt 0.657406 0.25 0 vt 0.370372 0.25 0 vt 0.379628 0.25 0 vt 1.009261 0.25 0 vt 0.009261 0.25 0 vt 0.620372 0.25 0 vt 0.629628 0.25 0 vt 0.018517 0.25 0 vt 0.981483 0.25 0 vt 0.398149 0.25 0 vt 0.407406 0.25 0 vt 0.990739 0.25 0 vt 0.037038 0.25 0 vt 0.592594 0.25 0 vt 0.601851 0.25 0 vt 0.046295 0.25 0 vt 0.953705 0.25 0 vt 0.425927 0.25 0 vt 0.435184 0.25 0 vt 0.962962 0.25 0 vt 0.064816 0.25 0 vt 0.564816 0.25 0 vt 0.574073 0.25 0 vt 0.074073 0.25 0 vt 0.925927 0.25 0 vt 0.453705 0.25 0 vt 0.462962 0.25 0 vt 0.935184 0.25 0 vt 0.092594 0.25 0 vt 0.537038 0.25 0 vt 0.546295 0.25 0 vt 0.101851 0.25 0 vt 0.898149 0.25 0 vt 0.481483 0.25 0 vt 0.490739 0.25 0 vt 0.907406 0.25 0 vt 0.120372 0.25 0 vt 0.509261 0.25 0 vt 0.518517 0.25 0 vt 0.129628 0.25 0 f 1/4 2/6 4/9 3/8 f 3/8 4/9 6/12 5/11 f 5/11 6/12 8/15 7/14 f 7/14 8/15 10/18 9/17 f 9/17 10/18 12/21 11/20 f 11/20 12/21 14/24 13/23 f 13/23 14/24 16/27 15/26 f 15/26 16/27 18/30 17/29 f 17/29 18/30 20/33 19/32 f 19/32 20/33 22/36 21/35 f 21/35 22/36 24/39 23/38 f 23/38 24/39 26/42 25/41 f 25/41 26/42 28/45 27/44 f 27/44 28/45 30/48 29/47 f 29/47 30/48 32/51 31/50 f 31/50 32/51 34/54 33/53 f 33/53 34/54 36/57 35/56 f 35/56 36/57 38/60 37/59 f 37/59 38/60 40/63 39/62 f 39/62 40/63 42/66 41/65 f 41/65 42/66 44/69 43/68 f 43/68 44/69 46/72 45/71 f 45/71 46/72 48/75 47/74 f 47/74 48/75 50/78 49/77 f 49/77 50/78 52/81 51/80 f 51/80 52/81 54/84 53/83 f 53/83 54/84 56/87 55/86 f 55/86 56/87 58/90 57/89 f 57/89 58/90 60/93 59/92 f 59/92 60/93 62/96 61/95 f 61/95 62/96 64/99 63/98 f 63/98 64/99 66/102 65/101 f 65/101 66/102 68/105 67/104 f 67/104 68/105 70/108 69/107 f 69/107 70/108 72/111 71/110 f 71/110 72/111 2/5 1/3 f 1/2 109/149 112/152 3/7 f 71/109 113/153 116/156 1/1 f 3/7 117/157 120/160 5/10 f 69/106 121/161 124/164 71/109 f 5/10 125/165 128/168 7/13 f 67/103 129/169 132/172 69/106 f 7/13 133/173 136/176 9/16 f 65/100 137/177 140/180 67/103 f 9/16 141/181 144/184 11/19 f 63/97 73/112 74/113 65/100 f 11/19 75/114 76/115 13/22 f 61/94 77/116 78/117 63/97 f 13/22 79/118 80/119 15/25 f 59/91 81/120 82/121 61/94 f 15/25 83/122 84/123 17/28 f 57/88 85/124 86/125 59/91 f 17/28 87/126 88/127 19/31 f 55/85 89/128 90/129 57/88 f 19/31 91/130 92/131 21/34 f 53/82 93/132 94/133 55/85 f 21/34 95/134 96/135 23/37 f 51/79 97/136 98/137 53/82 f 23/37 99/138 100/139 25/40 f 49/76 101/140 102/141 51/79 f 25/40 103/142 104/143 27/43 f 47/73 105/144 106/145 49/76 f 27/43 107/146 108/147 29/46 f 45/70 110/150 111/151 47/73 f 29/46 114/154 115/155 31/49 f 43/67 118/158 119/159 45/70 f 31/49 122/162 123/163 33/52 f 41/64 126/166 127/167 43/67 f 33/52 130/170 131/171 35/55 f 39/61 134/174 135/175 41/64 f 35/55 138/178 139/179 37/58 f 37/58 142/182 143/183 39/61 f 121/161 69/106 132/172 f 129/169 67/103 140/180 f 137/177 65/100 74/113 f 73/112 63/97 78/117 f 77/116 61/94 82/121 f 81/120 59/91 86/125 f 85/124 57/88 90/129 f 113/153 71/109 124/164 f 109/148 1/1 116/156 f 117/157 3/7 112/152 f 125/165 5/10 120/160 f 133/173 7/13 128/168 f 141/181 9/16 136/176 f 75/114 11/19 144/184 f 79/118 13/22 76/115 f 83/122 15/25 80/119 f 87/126 17/28 84/123 f 91/130 19/31 88/127 f 95/134 21/34 92/131 f 99/138 23/37 96/135 f 103/142 25/40 100/139 f 107/146 27/43 104/143 f 114/154 29/46 108/147 f 122/162 31/49 115/155 f 130/170 33/52 123/163 f 138/178 35/55 131/171 f 142/182 37/58 139/179 f 134/174 39/61 143/183 f 126/166 41/64 135/175 f 118/158 43/67 127/167 f 110/150 45/70 119/159 f 105/144 47/73 111/151 f 101/140 49/76 106/145 f 97/136 51/79 102/141 f 93/132 53/82 98/137 f 89/128 55/85 94/133 ================================================ FILE: static/models/controller.dae ================================================ CINEMA4D 16.050 COLLADA Exporter 2017-02-21T20:23:28Z 2017-02-21T20:23:28Z Y_UP 0.072 0.072 0.072 1 0.2 0.2 0.2 1 0.5 0.8 0.8 0.8 1 0.2 0.2 0.2 1 0.5 -2.2549 -0.1485 5.3795 -2.2535 -0.1485 4.2118 -1.8888 -0.1485 3.2558 -1.0171 -0.1485 2.5711 1.0171 -0.1485 2.5711 1.8888 -0.1485 3.2558 2.2535 -0.1485 4.2118 2.2549 -0.1485 5.3795 -0.9548 -0.1485 17.8157 -1.422 0.3515 17.186 0 0.3515 2.3695 1.0171 0.3515 2.5711 1.8888 0.3515 3.2558 2.2535 0.3515 4.2118 2.2549 0.3515 5.3795 1.422 0.3515 17.186 0.9548 0.3515 17.8157 0 0.3515 18.0805 -1.422 -0.1485 17.186 0 -0.1485 2.3695 1.422 -0.1485 17.186 0.9548 -0.1485 17.8157 0 -0.1485 18.0805 -2.2549 0.3515 5.3795 -2.2535 0.3515 4.2118 -1.8888 0.3515 3.2558 -1.0171 0.3515 2.5711 -0.9548 0.3515 17.8157 -2.0245 -0.1485 8.64552 2.0245 -0.1485 8.64552 -2.0245 0.3515 8.64552 2.0245 0.3515 8.64552 -2.14437 -0.1485 6.94628 2.14437 -0.1485 6.94628 2.14437 0.3515 6.94628 -2.14437 0.3515 6.94628 -1.48128 0.3515 16.3457 1.48128 0.3515 16.3457 -1.48128 -0.1485 16.3457 1.48128 -0.1485 16.3457 -1.33212 -1.14635 5.3795 -1.27843 -1.30407 4.2118 -1.61681 -0.76715 3.34645 -1.0171 -0.621514 2.66175 1.0171 -0.621514 2.66175 1.61681 -0.76715 3.34645 1.27843 -1.30407 4.2118 1.33212 -1.14635 5.3795 -0.9548 -0.520102 17.5876 -0.991745 -1.06956 16.9579 0 -0.621514 2.46015 0.991745 -1.06956 16.9579 0.9548 -0.520102 17.5876 0 -0.520102 17.8524 -1.57086 -1.55864 8.64552 1.57086 -1.55864 8.64552 -1.98998 -0.744035 6.94628 1.98998 -0.744035 6.94628 -1.0843 -1.14109 16.3457 1.0843 -1.14109 16.3457 -1.79939 -0.1485 11.8365 1.79942 -0.1485 11.836 -1.79939 0.3515 11.8365 1.79939 0.3515 11.8365 -1.02835 -1.73453 11.8365 1.02835 -1.73453 11.8365 -2.2549 0.0655521 5.3795 -2.2535 0.0655521 4.2118 -1.8888 0.0655521 3.2558 -1.0171 0.0655521 2.5711 1.0171 0.0655521 2.5711 1.8888 0.0655521 3.2558 2.2535 0.0655521 4.2118 2.2549 0.0655521 5.3795 -0.9548 0.0655521 17.8157 -1.422 0.0655521 17.186 0 0.0655521 2.3695 1.422 0.0655521 17.186 0.9548 0.0655521 17.8157 0 0.0655521 18.0805 -2.0245 0.0655521 8.64552 2.0245 0.0655521 8.64552 -2.14437 0.0655521 6.94628 2.14437 0.0655521 6.94628 -1.48128 0.0655521 16.3457 1.48128 0.0655521 16.3457 -1.79939 0.0655521 11.8365 1.79941 0.0655521 11.8362 -0.974563 -0.208981 0.080959 -0.977375 -0.197688 0.0752136 -0.997521 0 0.070371 -0.997521 0 0.070371 -0.983335 0 -0.181805 -0.983335 0 -0.181805 -0.999401 0 0.0346082 -0.999401 0 0.0346082 -0.935838 -0.320969 -0.145552 -0.802917 -0.215496 -0.555775 -0.805244 0 -0.592943 -0.417551 0 -0.908654 -0.426188 -0.117689 -0.896947 0 -0.094567 -0.995519 3.79776e-09 0 -1 0.417551 0 -0.908654 0 0 -1 0.432786 -0.11228 -0.894477 0.827368 -0.206634 -0.522269 0.805244 0 -0.592943 0.983335 0 -0.181805 0.983335 0 -0.181805 0.805244 0 -0.592943 0.999401 0 0.0346082 0.999401 0 0.0346082 0.997521 0 0.070371 0.915198 -0.213152 0.342021 0.561269 -0.249455 0.789145 0.565883 0 0.824485 0.937863 0 0.347005 0 -0.273096 0.961987 0 0 1 -0.565883 0 0.824485 -0.610889 -0.220693 0.760335 -0.937766 -0.181729 0.295922 -0.937863 0 0.347005 -0.565343 -0.745683 0.352624 -0.455322 -0.596463 0.660994 0.0232068 -0.660211 0.750721 0.412042 -0.782451 0.466897 0.41247 -0.581806 0.700979 0 -0.995218 -0.0976834 0.462019 -0.880465 0.106398 -0.460352 -0.884432 0.0765248 0.394919 -0.903483 -0.166606 -0.420097 -0.8866 -0.193544 0.636504 -0.621899 -0.456184 0.384969 -0.492232 -0.780709 0.0398219 -0.62209 -0.781933 -0.32359 -0.630516 -0.705507 -0.626206 -0.603683 -0.493389 0 1 -0 -0.997521 0 0.070371 -0.997521 0 0.070371 0.937863 0 0.347005 0.997521 2.30563e-08 0.070371 0.997521 -2.98855e-08 0.070371 0 -0.968653 -0.248418 0 -0.999282 0.037886 0.979776 -0.193463 0.0510887 0.982816 -0.164421 0.0839002 0.997521 -3.2879e-08 0.070371 0.997521 4.43539e-08 0.070371 0.997521 -6.27645e-08 0.070371 0 -0.992374 0.123262 -0.980656 -0.194762 0.0195223 -0.922808 -0.383862 -0.0327739 -0.953396 -0.301488 -0.0118799 0.953773 -0.300098 0.0161091 0.924102 -0.380991 0.0296838 0.942695 -0.314751 -0.110717 0.918888 -0.387507 0.0740439 0.911178 -0.402854 0.086397 0.975314 -0.205782 0.0801021 0.977509 -0.197613 0.0736611 -0.908355 -0.408845 0.0879633 -0.918574 -0.38766 0.0770846 0.941081 -0.324524 0.09514 -0.982972 -0.164557 0.0817758 -0.941396 -0.324809 0.0909489 0.997521 2.12976e-08 0.070371 0.0234698 1 0.149406 1 0.149406 0.571896 0.0234698 0.571896 0.362276 0.571896 0.362276 0 0.329744 0 0.329744 0.571896 0.362276 1 0.390782 1 0.390782 0.571896 0.421663 0.571896 0.421663 0 0.390782 0 0.421663 1 0.450551 1 0.450551 0.571896 0.479438 0.571896 0.479438 0 0.450551 0 0.479438 1 0.51032 1 0.51032 0.571896 0.538826 0.571896 0.538826 0 0.51032 0 0.571358 0.571896 0.571358 0 0.615116 0.571896 0.615116 0 0.901102 1 0.922946 1 0.922946 0.571896 0.901102 0.571896 0.950551 1 0.950551 0.571896 0.978155 0.571896 0.978155 0 0.950551 0 0.978155 1 1 1 1 0.571896 0.0530138 0.943065 0.0827509 0.983146 0.143524 1 0.234033 0.943065 0.204296 0.983146 0.280012 0.291311 0.287047 0.191586 0 0.191586 0.0070352 0.291311 0.286958 0.117262 8.91095e-05 0.117262 0.263745 0.0564127 0.208262 0.0128318 0.143524 0 0.0787856 0.0128318 0.0233021 0.0564127 0.0492405 0.889578 0.237807 0.889578 0.285985 0.571896 0.285985 0 0.238527 0 0.238527 0.571896 0.901102 0 0.877632 0 0.877632 0.571896 0.0146652 0.399467 0.0289934 0.602571 0.258054 0.602571 0.272382 0.399467 0.615116 1 0.662575 1 0.662575 0.571896 0.0234698 0 0 0 0 0.571896 0.751695 0 0.751688 0.571896 0.258056 0.60254 0.149406 0 0.751682 1 0.329744 1 0.538826 1 0.571358 1 0.922946 0 1 0 0.238527 1 0.285985 1 0.877632 1 0.662575 0 0 1

86 2 2 60 1 1 38 0 0 84 3 3 86 2 2 38 0 0 68 10 10 2 9 9 1 8 8 67 4 4 68 10 10 1 8 8 76 14 16 19 13 15 3 12 14 69 11 11 76 14 16 3 12 14 71 19 22 5 18 21 4 17 20 70 15 17 71 19 22 4 17 20 78 28 32 21 27 31 20 26 30 77 29 33 78 28 32 20 26 30 79 31 35 22 30 34 21 27 31 78 28 32 79 31 35 21 27 31 75 35 41 18 34 40 8 33 39 74 32 36 75 35 41 8 33 39 81 61 73 29 60 72 33 59 71 83 25 28 81 61 73 33 59 71 61 74 81 29 60 72 81 61 73 87 63 78 61 74 81 81 61 73 1 8 8 0 67 82 66 7 7 67 4 4 1 8 8 66 7 7 3 12 14 2 9 9 68 10 10 69 11 11 3 12 14 68 10 10 4 17 20 19 13 15 76 14 16 70 15 17 4 17 20 76 14 16 6 70 83 5 18 21 71 19 22 72 20 23 6 70 83 71 19 22 7 68 84 6 70 83 72 20 23 73 23 26 7 68 84 72 20 23 33 59 71 7 68 84 73 23 26 83 25 28 33 59 71 73 23 26 8 33 39 22 30 34 79 31 35 74 32 36 8 33 39 79 31 35 32 65 88 28 78 87 80 53 63 82 52 60 32 65 88 80 53 63 20 26 30 39 73 89 85 56 66 77 29 33 20 26 30 85 56 66 0 67 82 32 65 88 82 52 60 66 7 7 0 67 82 82 52 60 38 0 0 18 34 91 75 35 76 84 3 3 38 0 0 75 35 76 39 73 89 61 74 81 87 63 78 85 56 66 39 73 89 87 63 78 28 78 87 60 1 1 86 2 2 80 53 63 28 78 87 86 2 2

23 6 6 24 5 5 67 4 4 66 7 7 23 6 6 67 4 4 25 10 13 26 11 12 69 11 11 68 10 10 25 10 13 69 11 11 10 16 19 11 15 18 70 15 17 76 14 16 10 16 19 70 15 17 12 22 25 13 21 24 72 20 23 71 19 22 12 22 25 72 20 23 13 21 24 14 24 27 73 23 26 72 20 23 13 21 24 73 23 26 14 24 27 34 25 29 83 25 28 73 23 26 14 24 27 83 25 28 17 31 38 27 32 37 74 32 36 79 31 35 17 31 38 74 32 36 53 38 44 48 37 43 49 36 42 51 39 45 53 38 44 49 36 42 51 39 45 52 40 46 53 38 44 40 43 49 47 42 48 57 41 47 56 41 50 40 43 49 57 41 47 46 44 51 47 42 48 40 43 49 41 45 52 46 44 51 40 43 49 44 47 54 45 46 53 46 44 51 50 48 55 44 47 54 46 44 51 50 48 55 46 44 51 41 45 52 43 49 56 50 48 55 41 45 52 41 45 52 42 50 57 43 49 56 37 51 59 36 51 58 9 51 42 15 51 45 37 51 59 9 51 42 15 51 45 9 51 42 17 51 44 16 51 46 15 51 45 17 51 44 27 51 43 17 51 44 9 51 42 24 51 52 23 51 49 14 51 48 13 51 51 24 51 52 14 51 48 26 51 56 25 51 57 24 51 52 10 51 55 26 51 56 24 51 52 10 51 55 24 51 52 13 51 51 11 51 54 10 51 55 13 51 51 13 51 51 12 51 53 11 51 54 30 53 62 35 52 61 82 52 60 80 53 63 30 53 62 82 52 60 37 55 65 15 54 64 77 29 33 85 56 66 37 55 65 77 29 33 65 58 69 64 58 68 54 57 67 55 57 70 65 58 69 54 57 67 14 51 48 23 51 49 35 51 50 34 51 47 14 51 48 35 51 50 55 57 70 54 57 67 56 41 50 57 41 47 55 57 70 56 41 50 35 52 61 23 6 6 66 7 7 82 52 60 35 52 61 66 7 7 30 51 67 31 51 70 34 51 47 35 51 50 30 51 67 34 51 47 75 35 76 9 35 75 36 3 74 84 3 3 75 35 76 36 3 74 63 51 69 62 51 68 36 51 58 37 51 59 63 51 69 36 51 58 87 63 78 63 62 77 37 55 65 85 56 66 87 63 78 37 55 65 51 39 45 49 36 42 58 64 58 59 64 59 51 39 45 58 64 58 48 37 43 53 38 44 22 30 44 8 33 43 48 37 43 22 30 44 49 36 42 48 37 43 8 33 43 18 34 42 49 36 42 8 33 43 52 40 46 51 39 45 20 26 45 21 27 46 52 40 46 20 26 45 53 38 44 52 40 46 21 27 46 22 30 44 53 38 44 21 27 46 40 43 49 56 66 50 32 65 50 0 67 49 40 43 49 32 65 50 57 69 47 47 42 48 7 68 48 33 59 47 57 69 47 7 68 48 41 45 52 40 43 49 0 67 49 1 8 52 41 45 52 0 67 49 47 42 48 46 44 51 6 70 51 7 68 48 47 42 48 6 70 51 44 47 54 50 48 55 19 13 55 4 17 54 44 47 54 19 13 55 45 46 53 44 47 54 4 17 54 5 18 53 45 46 53 4 17 54 46 44 51 45 46 53 5 18 53 6 70 51 46 44 51 5 18 53 50 48 55 43 49 56 3 12 56 19 13 55 50 48 55 3 12 56 42 50 57 41 45 52 1 8 52 2 9 57 42 50 57 1 8 52 43 49 56 42 50 57 2 9 57 3 12 56 43 49 56 2 9 57 39 73 59 59 72 59 65 71 69 61 74 79 39 73 59 65 71 69 64 76 68 58 75 58 38 0 58 60 1 68 64 76 68 38 0 58 55 77 70 57 69 47 33 59 47 29 60 70 55 77 70 33 59 47 56 66 50 54 79 67 28 78 67 32 65 50 56 66 50 28 78 67 51 39 45 59 72 59 39 73 59 20 26 45 51 39 45 39 73 59 58 75 58 49 36 42 18 34 42 38 0 58 58 75 58 18 34 42 62 2 80 30 53 62 80 53 63 86 2 2 62 2 80 80 53 63 59 64 59 58 64 58 64 58 68 65 58 69 59 64 59 64 58 68 31 51 70 30 51 67 62 51 68 63 51 69 31 51 70 62 51 68 55 77 70 29 60 70 61 74 79 65 71 69 55 77 70 61 74 79 28 78 67 54 79 67 64 76 68 60 1 68 28 78 67 64 76 68 36 3 74 62 2 80 86 2 2 84 3 3 36 3 74 86 2 2 24 5 5 25 10 13 68 10 10 67 4 4 24 5 5 68 10 10 26 11 12 10 16 19 76 14 16 69 11 11 26 11 12 76 14 16 11 15 18 12 22 25 71 19 22 70 15 17 11 15 18 71 19 22 15 54 64 16 28 85 78 28 32 77 29 33 15 54 64 78 28 32 16 28 85 17 31 38 79 31 35 78 28 32 16 28 85 79 31 35 27 32 37 9 35 86 75 35 41 74 32 36 27 32 37 75 35 41 34 25 29 31 80 90 81 61 73 83 25 28 34 25 29 81 61 73 31 80 90 63 62 77 87 63 78 81 61 73 31 80 90 87 63 78

0 0 -0 0 1 0 -0 1 0 0 0 0 0 1 -0 1 1 1
================================================ FILE: static/models/door-frame.obj ================================================ # Blender v2.76 (sub 0) OBJ File: '' # www.blender.org o DoorFrame v 2.860165 0.000004 -0.260001 v 2.704165 0.000004 -0.260001 v 2.704165 5.412390 -0.260000 v 2.860165 5.568390 -0.260000 v 2.704165 0.000004 0.259999 v 2.704165 5.412390 0.260000 v 2.860165 0.000004 0.259999 v 2.860165 5.568390 0.260000 v -0.164165 5.412390 -0.260000 v -0.320165 5.568390 -0.260000 v -0.164165 5.412390 0.260000 v -0.320165 5.568390 0.260000 v -0.164165 0.000004 -0.260001 v -0.320165 0.000004 -0.260001 v -0.164165 0.000004 0.259999 v -0.320165 0.000004 0.259999 v 2.860165 0.000004 0.259999 v 2.704165 0.000004 0.259999 v 2.704165 0.000004 -0.260001 v 2.860165 0.000004 -0.260001 v -0.320165 0.000004 -0.260001 v -0.164165 0.000004 -0.260001 v -0.320165 0.000004 0.259999 v -0.164165 0.000004 0.259999 vn -0.000000 0.000000 -1.000000 vn 0.000000 -1.000000 -0.000000 vn 0.000000 -0.000000 1.000000 vn -0.000000 1.000000 0.000000 vn 1.000000 0.000000 -0.000000 vn -1.000000 -0.000000 0.000000 s 1 f 1//1 2//1 3//1 f 5//2 6//2 3//2 f 7//3 8//3 6//3 f 1//4 4//4 8//4 f 3//1 9//1 10//1 f 6//5 11//5 9//5 f 8//3 12//3 11//3 f 4//6 10//6 12//6 f 9//1 13//1 14//1 f 11//4 15//4 13//4 f 11//3 12//3 16//3 f 10//2 14//2 16//2 f 17//5 18//5 19//5 f 17//5 19//5 20//5 f 21//5 22//5 23//5 f 22//5 24//5 23//5 f 4//1 1//1 3//1 f 2//2 5//2 3//2 f 5//3 7//3 6//3 f 7//4 1//4 8//4 f 4//1 3//1 10//1 f 3//5 6//5 9//5 f 6//3 8//3 11//3 f 8//6 4//6 12//6 f 10//1 9//1 14//1 f 9//4 11//4 13//4 f 15//3 11//3 16//3 f 12//2 10//2 16//2 ================================================ FILE: static/models/door-glow.obj ================================================ # Blender v2.78 (sub 0) OBJ File: '' # www.blender.org o Glow.001_ID7.001 v 3.023290 -0.039070 -1.075025 v 3.033190 5.163200 -1.075235 v -1.377980 -0.039075 0.000000 v -1.377980 5.163200 0.000000 v 1.370000 -0.039075 -0.255000 v 1.370000 5.163200 -0.255000 vt 0.0000 0.0000 vt 0.0000 0.0000 vt 0.0000 1.0000 vt 0.0000 1.0000 vt 1.0000 1.0000 vt 1.0000 0.0000 vn -0.2727 0.0003 -0.9621 vn -0.0924 0.0000 -0.9957 vn -0.2719 0.0000 -0.9623 vn -0.4427 0.0002 -0.8966 vn -0.4443 0.0008 -0.8958 s 1 f 5/1/1 3/2/2 4/3/2 f 6/4/3 5/1/1 4/3/2 f 2/5/4 1/6/5 5/1/1 f 6/4/3 2/5/4 5/1/1 ================================================ FILE: static/models/door.dae ================================================ CINEMA4D 16.050 COLLADA Exporter 2017-02-21T18:55:36Z 2017-02-21T18:55:36Z Y_UP 0.8 0.8 0.8 1 0.2 0.2 0.2 1 0.5 0.136 0.136 0.136 1 0.2 0.2 0.2 1 0.5 0.8 0.8 0.8 1 0.2 0.2 0.2 1 0.5 231.421 250.569 37.2362 14.7678 34.7234 -16.7227 244.453 242.515 -42.2138 239.475 229.484 24.2048 270.966 4.07145 -16.7227 270.966 542.094 -16.7227 143.006 34.7234 -16.7227 -16.2139 542.094 -16.7227 239.984 511.442 -16.7227 111.745 34.7234 -16.7227 247.529 234.461 -21.1285 -16.2139 4.07145 -16.7227 14.7678 214.474 -16.7227 252.507 237.537 -29.1824 247.529 234.461 -37.2362 234.498 242.515 -42.2138 14.7678 34.7234 -16.7227 239.475 229.484 -34.1599 231.421 234.461 -37.2362 143.006 34.7234 -16.7227 226.444 247.493 29.1824 111.745 34.7234 -16.7227 239.984 266.576 -16.7227 239.984 34.7234 -16.7227 143.006 511.442 -16.7227 14.7678 214.474 -16.7227 14.7678 266.576 -16.7227 143.006 214.474 -16.7227 226.444 237.537 -29.1824 231.421 250.569 21.1285 111.745 266.576 -16.7227 231.421 234.461 -21.1285 234.498 242.515 -16.151 244.453 242.515 -16.151 239.984 34.7234 -16.7227 143.006 214.474 -16.7227 111.745 511.442 -16.7227 239.984 511.442 -16.7227 239.984 266.576 -16.7227 143.006 511.442 -16.7227 14.7678 266.576 -16.7227 14.7678 511.442 -16.7227 111.745 266.576 -16.7227 14.7678 511.442 -16.7227 143.006 266.576 -16.7227 111.745 214.474 -16.7227 239.984 214.474 -16.7227 143.006 266.576 -16.7227 111.745 214.474 -16.7227 239.984 214.474 -16.7227 111.745 511.442 -16.7227 239.475 249.849 -17.3161 239.475 235.181 -17.3161 251.342 242.515 -21.8486 251.342 242.515 -36.5162 239.475 249.849 -41.0487 239.475 235.181 -41.0487 227.609 242.515 -36.5162 227.609 242.515 -21.8486 246.809 254.381 -29.1824 232.141 254.381 -29.1824 232.141 230.649 -29.1824 246.809 230.649 -29.1824 242.29 256.178 -29.1824 236.661 256.178 -29.1824 242.29 252.699 -20.0739 245.105 254.438 -24.6281 236.661 252.699 -20.0739 233.846 254.438 -24.6281 251.398 247.069 -23.553 249.659 251.623 -26.3677 244.029 248.144 -17.2592 248.584 245.33 -18.9988 251.398 247.069 -34.8117 249.659 251.623 -31.9971 253.138 242.515 -26.3677 253.138 242.515 -31.9971 242.29 252.699 -38.2909 245.105 254.438 -33.7366 248.584 245.33 -39.366 244.029 248.144 -41.1056 236.661 252.699 -38.2909 233.846 254.438 -33.7366 227.552 247.069 -34.8117 229.292 251.623 -31.9971 234.921 248.144 -41.1056 230.367 245.33 -39.366 227.552 247.069 -23.553 229.292 251.623 -26.3677 225.812 242.515 -31.9971 225.812 242.515 -26.3677 234.921 248.144 -17.2592 230.367 245.33 -18.9988 236.661 228.852 -29.1824 242.29 228.852 -29.1824 236.661 232.331 -20.0739 233.846 230.592 -24.6281 242.29 232.331 -20.0739 245.105 230.592 -24.6281 251.398 237.961 -23.553 249.659 233.406 -26.3677 244.029 236.886 -17.2592 248.584 239.7 -18.9988 251.398 237.961 -34.8117 249.659 233.406 -31.9971 248.584 239.7 -39.366 244.029 236.886 -41.1056 242.29 232.331 -38.2909 245.105 230.592 -33.7366 236.661 232.331 -38.2909 233.846 230.592 -33.7366 234.921 236.886 -41.1056 230.367 239.7 -39.366 227.552 237.961 -34.8117 229.292 233.406 -31.9971 227.552 237.961 -23.553 229.292 233.406 -26.3677 234.921 236.886 -17.2592 230.367 239.7 -18.9988 239.475 245.33 -15.5196 239.475 239.7 -15.5196 239.475 245.33 -42.8451 239.475 239.7 -42.8451 239.475 255.546 -24.2048 247.529 250.569 -21.1285 252.507 247.493 -29.1824 247.529 250.569 -37.2362 239.475 255.546 -34.1599 231.421 250.569 -37.2362 226.444 247.493 -29.1824 231.421 250.569 -21.1285 239.475 229.484 -24.2048 14.7678 34.7234 16.7227 244.453 242.515 42.2138 270.966 4.07145 16.7227 270.966 542.094 16.7227 143.006 34.7234 16.7227 -16.2139 542.094 16.7227 239.984 511.442 16.7227 111.745 34.7234 16.7227 247.529 234.461 21.1285 -16.2139 4.07145 16.7227 14.7678 214.474 16.7227 252.507 237.537 29.1824 247.529 234.461 37.2362 234.498 242.515 42.2138 14.7678 34.7234 16.7227 239.475 229.484 34.1599 231.421 234.461 37.2362 143.006 34.7234 16.7227 111.745 34.7234 16.7227 239.984 266.576 16.7227 239.984 34.7234 16.7227 143.006 511.442 16.7227 14.7678 214.474 16.7227 14.7678 266.576 16.7227 143.006 214.474 16.7227 226.444 237.537 29.1824 111.745 266.576 16.7227 231.421 234.461 21.1285 234.498 242.515 16.151 244.453 242.515 16.151 239.984 34.7234 16.7227 143.006 214.474 16.7227 111.745 511.442 16.7227 239.984 511.442 16.7227 239.984 266.576 16.7227 143.006 511.442 16.7227 14.7678 266.576 16.7227 14.7678 511.442 16.7227 111.745 266.576 16.7227 14.7678 511.442 16.7227 143.006 266.576 16.7227 111.745 214.474 16.7227 239.984 214.474 16.7227 143.006 266.576 16.7227 111.745 214.474 16.7227 239.984 214.474 16.7227 111.745 511.442 16.7227 239.475 249.849 17.3161 239.475 235.181 17.3161 251.342 242.515 21.8486 251.342 242.515 36.5162 239.475 249.849 41.0487 239.475 235.181 41.0487 227.609 242.515 36.5162 227.609 242.515 21.8486 246.809 254.381 29.1824 232.141 254.381 29.1824 232.141 230.649 29.1824 246.809 230.649 29.1824 242.29 256.178 29.1824 236.661 256.178 29.1824 242.29 252.699 20.0739 245.105 254.438 24.6281 236.661 252.699 20.0739 233.846 254.438 24.6281 251.398 247.069 23.553 249.659 251.623 26.3677 244.029 248.144 17.2592 248.584 245.33 18.9988 251.398 247.069 34.8117 249.659 251.623 31.9971 253.138 242.515 26.3677 253.138 242.515 31.9971 242.29 252.699 38.2909 245.105 254.438 33.7366 248.584 245.33 39.366 244.029 248.144 41.1056 236.661 252.699 38.2909 233.846 254.438 33.7366 227.552 247.069 34.8117 229.292 251.623 31.9971 234.921 248.144 41.1056 230.367 245.33 39.366 227.552 247.069 23.553 229.292 251.623 26.3677 225.812 242.515 31.9971 225.812 242.515 26.3677 234.921 248.144 17.2592 230.367 245.33 18.9988 236.661 228.852 29.1824 242.29 228.852 29.1824 236.661 232.331 20.0739 233.846 230.592 24.6281 242.29 232.331 20.0739 245.105 230.592 24.6281 251.398 237.961 23.553 249.659 233.406 26.3677 244.029 236.886 17.2592 248.584 239.7 18.9988 251.398 237.961 34.8117 249.659 233.406 31.9971 248.584 239.7 39.366 244.029 236.886 41.1056 242.29 232.331 38.2909 245.105 230.592 33.7366 236.661 232.331 38.2909 233.846 230.592 33.7366 234.921 236.886 41.1056 230.367 239.7 39.366 227.552 237.961 34.8117 229.292 233.406 31.9971 227.552 237.961 23.553 229.292 233.406 26.3677 234.921 236.886 17.2592 230.367 239.7 18.9988 239.475 245.33 15.5196 239.475 239.7 15.5196 239.475 245.33 42.8451 239.475 239.7 42.8451 239.475 255.546 24.2048 247.529 250.569 21.1285 252.507 247.493 29.1824 247.529 250.569 37.2362 239.475 255.546 34.1599 0 0 -1 -2.57687e-07 0.195614 -0.980681 -2.52584e-07 -0.195614 -0.980681 0.356822 7.66799e-09 -0.934172 0.332085 0.400854 -0.853836 0.356822 0 -0.934172 0.648596 -0.20524 -0.732939 0.648596 0.20524 -0.732939 0.850651 -3.05074e-09 -0.525731 0.648596 0.20524 -0.732939 -2.55136e-07 0.195614 -0.980681 -2.55136e-07 -0.195614 -0.980681 -1.98298e-07 -0.525731 -0.850651 0.332085 -0.400854 -0.853836 0.332085 -0.400855 -0.853836 0.332085 0.400855 -0.853836 0.332085 0.400855 0.853836 -2.52584e-07 0.195614 0.980681 -1.92197e-07 0.525731 0.850651 0.648596 -0.20524 -0.732939 0.850651 -6.10148e-09 -0.525731 -1.98298e-07 0.525731 -0.850651 -0.332086 0.400854 -0.853836 -0.648596 0.20524 -0.732939 -0.356822 5.11199e-09 -0.934172 -0.850651 -3.05074e-09 -0.525731 -0.648596 -0.20524 -0.732939 0 0 -0 -1.89146e-07 -0.525731 -0.850651 -0.332086 -0.400854 -0.853835 -0.525732 0.85065 -1.83044e-08 -0.195615 0.980681 5.10272e-09 -0.400855 0.853835 0.332086 0.195615 0.980681 2.55136e-09 -1.84032e-07 0.934172 0.356823 -0.205241 0.73294 0.648595 0.525732 0.85065 -2.74567e-08 0.400855 0.853835 0.332085 0.20524 0.73294 0.648595 0.732939 0.648596 0.20524 0.853836 0.332086 0.400854 0.57735 0.577351 0.57735 0.850651 -6.10148e-09 0.525731 0.648596 0.20524 0.732939 0.732939 0.648596 -0.20524 0.853836 0.332086 -0.400854 0.934172 0.356823 2.556e-08 0.980681 -2.55136e-09 -0.195613 0.980681 -2.55136e-09 0.195613 0.400855 0.853835 -0.332085 0.20524 0.73294 -0.648595 0.57735 0.577351 -0.57735 -1.81476e-07 0.934172 -0.356823 -0.400855 0.853835 -0.332086 -0.205241 0.73294 -0.648595 -0.57735 0.57735 -0.57735 -0.732939 0.648596 -0.205241 -0.853835 0.332085 -0.400855 -0.980681 -2.55136e-09 -0.195614 -0.934173 0.356822 2.556e-08 -0.980681 -2.55136e-09 0.195614 -0.732939 0.648596 0.205241 -0.853835 0.332085 0.400855 -0.850651 -6.10148e-09 0.525731 -0.57735 0.57735 0.57735 -0.332086 0.400854 0.853836 -0.648596 0.20524 0.732939 0.525732 -0.85065 -2.13552e-08 0.195615 -0.980681 7.65408e-09 0.400855 -0.853835 0.332085 -0.195615 -0.980681 5.10272e-09 -1.81476e-07 -0.934172 0.356823 0.20524 -0.73294 0.648595 -0.525732 -0.85065 -1.52537e-08 -0.400855 -0.853835 0.332086 -0.205241 -0.73294 0.648595 -1.92197e-07 -0.525731 0.850651 0.853836 -0.332086 0.400854 0.648596 -0.20524 0.732939 0.732939 -0.648596 0.20524 0.57735 -0.577351 0.57735 0.332085 -0.400855 0.853836 0.934172 -0.356823 2.8116e-08 0.853836 -0.332086 -0.400854 0.732939 -0.648596 -0.20524 0.57735 -0.577351 -0.57735 0.20524 -0.73294 -0.648595 0.400855 -0.853835 -0.332085 -0.205241 -0.73294 -0.648595 -0.400855 -0.853835 -0.332086 -1.84032e-07 -0.934172 -0.356823 -0.57735 -0.57735 -0.57735 -0.853835 -0.332085 -0.400855 -0.732939 -0.648596 -0.205241 -0.934173 -0.356822 2.8116e-08 -0.853835 -0.332085 0.400855 -0.732939 -0.648596 0.205241 -0.648596 -0.20524 0.732939 -0.332086 -0.400854 0.853835 -0.57735 -0.57735 0.57735 -2.55136e-07 -0.195614 0.980681 -0.356822 -2.556e-09 0.934172 0.356822 0 0.934172 0 0 1 -2.60239e-07 0.195614 0.980681 0.356822 7.66799e-09 0.934172 -2.57687e-07 -0.195614 0.980681 0.332085 0.400854 0.853836 0 -1 -0 1 0 -0 0.648596 0.20524 0.732939 0 1 -0 0.332085 -0.400854 0.853836 -1.98298e-07 -0.525731 0.850651 -1.92197e-07 0.525731 -0.850651 0.648596 -0.20524 0.732939 0.850651 -3.05074e-09 0.525731 -1.95247e-07 0.525731 0.850651 -0.332086 0.400854 0.853835 -0.356822 5.11199e-09 0.934172 -0.648596 0.20524 0.732939 -0.648596 -0.20524 0.732939 -0.850651 0 0.525731 -1 0 -0 -0.332086 -0.400854 0.853835 -0.525732 0.85065 1.83044e-08 -0.400855 0.853835 -0.332086 -0.195615 0.980681 -7.65408e-09 -1.84032e-07 0.934172 -0.356823 0.195615 0.980681 -2.55136e-09 -0.205241 0.73294 -0.648595 0.400855 0.853835 -0.332085 0.525732 0.85065 2.74567e-08 0.20524 0.73294 -0.648595 0.732939 0.648596 -0.20524 0.853836 0.332086 -0.400853 0.732939 0.648596 0.20524 0.934172 0.356823 -2.3004e-08 0.853836 0.332086 0.400854 0.980681 -2.55136e-09 0.195613 0.980681 0 -0.195613 0.400855 0.853835 0.332085 0.57735 0.577351 0.57735 0.20524 0.73294 0.648595 -1.86588e-07 0.934172 0.356823 -0.400855 0.853835 0.332086 -0.205241 0.73294 0.648595 -0.57735 0.57735 0.57735 -0.732939 0.648596 0.205241 -0.853835 0.332085 0.400855 -0.980681 2.55136e-09 0.195614 -0.934173 0.356822 -2.8116e-08 -0.980681 0 -0.195614 -0.732939 0.648596 -0.205241 -0.853835 0.332085 -0.400855 -0.850651 -3.05074e-09 -0.525731 -0.57735 0.57735 -0.57735 -0.332086 0.400854 -0.853835 -0.648596 0.20524 -0.732939 0.525732 -0.85065 2.13552e-08 0.400855 -0.853835 -0.332085 0.195615 -0.980681 -5.10272e-09 -1.86588e-07 -0.934172 -0.356823 -0.195615 -0.980681 -2.55136e-09 0.20524 -0.73294 -0.648595 -0.400855 -0.853835 -0.332086 -0.525732 -0.85065 1.2203e-08 -0.205241 -0.73294 -0.648595 0.853836 -0.332086 -0.400853 0.732939 -0.648596 -0.20524 0.934172 -0.356823 -2.8116e-08 0.853836 -0.332086 0.400854 0.732939 -0.648596 0.20524 0.57735 -0.577351 0.57735 0.20524 -0.73294 0.648595 0.400855 -0.853835 0.332085 -0.205241 -0.73294 0.648595 -1.86588e-07 -0.934172 0.356823 -0.400855 -0.853835 0.332086 -0.853835 -0.332085 0.400855 -0.732939 -0.648596 0.205241 -0.934173 -0.356822 -2.8116e-08 -0.853835 -0.332085 -0.400855 -0.732939 -0.648596 -0.205241 -0.648596 -0.20524 -0.732939 -0.57735 -0.57735 -0.57735 -0.332086 -0.400854 -0.853836 -0.356822 -2.556e-09 -0.934172 0 0.75 0.25 0.493175 0 0.25 0.333333 1 0.666667 1 0.5 0.666667 0.166667 0.666667 0.25 0.75 0 1 0.333333 0.333333 0.25 0.438529 0.666667 0.333333 0.833333 0.666667 0.25 0.25 0.476866 0.25 0.375 0 0.25 0.5 0.465299 0.75 1 1 0.465299 0.438529 0.465299 0.25 0.75 0.493175 0.75 0.75 1 0.75 1 0.25 0.75 0.25 0.75 0.438529 0.534701 0.25 0.534701 0.438529 0.5 0 0.534701 0.75 0.75 0.5 0 0.5 0.5 0.75 0.75 1 0.75 0 0.5 0.25 0.465299 0.493175 0.5 0.438529 0.534701 0.493175 0.375 1 0.476866 0.75 0.5 1 1 0 0 0 0.5 0.493175 0.534701 0.5 0.25 1 0.25 0

4 0 2 38 0 1 5 0 0 38 0 1 37 0 7 5 0 0 4 0 2 46 0 10 38 0 1 4 0 15 6 0 14 23 0 13 7 0 23 43 0 22 40 0 21 12 0 26 1 0 25 11 0 24 39 0 33 44 0 0 42 0 32 36 0 30 7 0 34 39 0 33 9 0 27 6 0 36 11 0 35 9 0 27 45 0 28 6 0 36 45 0 28 12 0 26 40 0 21 44 0 37 38 0 1 46 0 10 45 0 28 42 0 39 27 0 38 4 0 2 23 0 13 46 0 10 39 0 41 5 0 40 37 0 7 7 0 34 5 0 42 39 0 33 44 27 44 39 27 43 39 27 43 40 0 21 12 0 26 11 0 24 6 0 36 4 0 29 11 0 35 45 0 28 27 0 38 6 0 36 42 0 39 44 0 45 27 0 38 36 0 30 43 0 22 7 0 34 11 0 24 7 0 23 40 0 21 11 0 35 1 0 25 9 0 27 36 0 30 39 0 33 42 0 32 42 0 39 45 0 28 40 0 21 27 0 19 44 0 37 46 0 10 39 27 43 44 27 44 39 27 43 166 103 1 134 103 2 135 103 0 134 108 29 141 108 48 11 108 47 165 103 7 166 103 1 135 103 0 135 109 23 134 109 24 4 109 2 174 103 10 134 103 2 166 103 1 135 111 42 5 111 29 137 111 47 136 103 14 134 103 15 152 103 13 171 103 22 137 103 23 168 103 21 132 103 25 142 103 26 141 103 24 4 108 42 134 108 29 11 108 47 5 109 0 135 109 23 4 109 2 172 103 0 167 103 33 170 103 32 137 103 34 164 103 30 167 103 33 136 103 36 139 103 27 141 103 35 173 103 28 139 103 27 136 103 36 142 103 26 173 103 28 168 103 21 166 103 1 172 103 37 174 103 10 170 103 39 173 103 28 156 103 38 152 103 13 134 103 2 174 103 10 135 103 40 167 103 41 165 103 7 7 123 23 11 123 24 141 123 2 135 103 42 137 103 34 167 103 33 167 27 43 172 27 44 167 27 43 142 103 26 168 103 21 141 103 24 134 103 29 136 103 36 141 103 35 156 103 38 173 103 28 136 103 36 172 103 45 170 103 39 156 103 38 5 111 29 7 111 48 137 111 47 137 123 0 7 123 23 141 123 2 171 103 22 164 103 30 137 103 34 137 103 23 141 103 24 168 103 21 132 103 25 141 103 35 139 103 27 167 103 33 164 103 30 170 103 32 173 103 28 170 103 39 168 103 21 172 103 37 156 103 19 174 103 10 172 27 44 167 27 43 167 27 43

2 3 5 122 2 4 121 1 3 121 1 3 80 4 6 2 3 5 200 7 6 230 6 3 161 5 5 230 6 3 200 7 6 181 8 8 79 9 9 2 3 5 80 4 6 248 11 12 247 10 11 161 5 5 24 0 17 8 0 7 47 0 16 106 13 12 56 12 18 122 2 4 19 0 20 35 0 19 34 0 13 48 0 28 21 0 27 25 0 26 229 14 4 161 5 5 230 6 3 199 15 9 161 5 5 247 10 11 51 18 29 119 17 11 71 16 9 26 0 31 41 0 22 50 0 30 122 2 4 2 3 5 106 13 12 105 19 11 106 13 12 2 3 5 2 3 5 79 9 9 105 19 11 54 20 29 105 19 11 79 9 9 121 1 6 85 22 3 55 21 8 21 0 27 16 0 25 25 0 26 15 24 5 86 23 4 85 22 3 85 22 3 121 1 6 15 24 5 122 2 9 15 24 5 121 1 6 112 26 12 57 25 18 86 23 4 86 23 4 15 24 5 112 26 12 161 5 5 199 15 9 200 7 6 180 28 18 248 11 12 229 14 4 111 29 11 112 26 12 15 24 5 161 5 5 229 14 4 248 11 12 15 24 5 122 2 9 111 29 11 8 0 7 22 0 7 47 0 16 56 12 29 111 29 11 122 2 9 35 0 19 49 0 10 34 0 13 30 0 46 26 0 31 50 0 30 80 4 6 121 1 3 55 21 8 68 32 6 64 31 3 60 30 8 123 34 5 63 33 4 64 31 3 64 31 3 68 32 6 123 34 5 67 35 9 123 34 5 68 32 6 66 37 12 59 36 18 63 33 4 63 33 4 123 34 5 66 37 12 65 38 11 66 37 12 123 34 5 123 34 5 67 35 9 65 38 11 51 18 29 65 38 11 67 35 9 66 37 6 70 39 3 59 36 8 124 41 5 69 40 4 70 39 3 70 39 3 66 37 6 124 41 5 65 38 9 124 41 5 66 37 6 72 43 12 53 42 18 69 40 4 69 40 4 124 41 5 72 43 12 71 16 11 72 43 12 124 41 5 124 41 5 65 38 9 71 16 11 51 18 29 71 16 11 65 38 9 70 39 6 74 44 3 59 36 8 125 46 5 73 45 4 74 44 3 74 44 3 70 39 6 125 46 5 69 40 9 125 46 5 70 39 6 76 47 12 54 20 18 73 45 4 73 45 4 125 46 5 76 47 12 75 48 11 76 47 12 125 46 5 125 46 5 69 40 9 75 48 11 53 42 29 75 48 11 69 40 9 74 44 6 78 49 3 59 36 8 126 51 5 77 50 4 78 49 3 78 49 3 74 44 6 126 51 5 73 45 9 126 51 5 74 44 6 80 4 12 55 21 18 77 50 4 77 50 4 126 51 5 80 4 12 79 9 11 80 4 12 126 51 5 126 51 5 73 45 9 79 9 11 54 20 29 79 9 11 73 45 9 78 49 6 63 33 3 59 36 8 127 52 5 64 31 4 63 33 3 63 33 3 78 49 6 127 52 5 77 50 9 127 52 5 78 49 6 82 53 12 60 30 18 64 31 4 64 31 4 127 52 5 82 53 12 81 54 11 82 53 12 127 52 5 127 52 5 77 50 9 81 54 11 55 21 29 81 54 11 77 50 9 85 22 6 81 54 3 55 21 8 128 55 5 82 53 4 81 54 3 81 54 3 85 22 6 128 55 5 86 23 9 128 55 5 85 22 6 84 56 12 60 30 18 82 53 4 82 53 4 128 55 5 84 56 12 83 57 11 84 56 12 128 55 5 128 55 5 86 23 9 83 57 11 57 25 29 83 57 11 86 23 9 89 58 6 83 57 3 57 25 8 129 59 5 84 56 4 83 57 3 83 57 3 89 58 6 129 59 5 90 60 9 129 59 5 89 58 6 88 61 12 60 30 18 84 56 4 84 56 4 129 59 5 88 61 12 87 62 11 88 61 12 129 59 5 129 59 5 90 60 9 87 62 11 58 63 29 87 62 11 90 60 9 88 61 6 68 32 3 60 30 8 130 64 5 67 35 4 68 32 3 68 32 3 88 61 6 130 64 5 87 62 9 130 64 5 88 61 6 91 65 12 51 18 18 67 35 4 67 35 4 130 64 5 91 65 12 92 66 11 91 65 12 130 64 5 130 64 5 87 62 9 92 66 11 58 63 29 92 66 11 87 62 9 98 69 6 94 68 3 62 67 8 131 71 5 93 70 4 94 68 3 94 68 3 98 69 6 131 71 5 97 72 9 131 71 5 98 69 6 96 74 12 61 73 18 93 70 4 93 70 4 131 71 5 96 74 12 95 75 11 96 74 12 131 71 5 131 71 5 97 72 9 95 75 11 52 76 29 95 75 11 97 72 9 102 78 6 99 77 3 53 42 8 10 80 5 100 79 4 99 77 3 99 77 3 102 78 6 10 80 5 101 81 9 10 80 5 102 78 6 98 69 12 62 67 18 100 79 4 100 79 4 10 80 5 98 69 12 97 72 11 98 69 12 10 80 5 10 80 5 101 81 9 97 72 11 52 76 29 97 72 11 101 81 9 99 77 6 75 48 3 53 42 8 13 82 5 76 47 4 75 48 3 75 48 3 99 77 6 13 82 5 100 79 9 13 82 5 99 77 6 103 83 12 54 20 18 76 47 4 76 47 4 13 82 5 103 83 12 104 84 11 103 83 12 13 82 5 13 82 5 100 79 9 104 84 11 62 67 29 104 84 11 100 79 9 103 83 6 105 19 3 54 20 8 14 85 5 106 13 4 105 19 3 105 19 3 103 83 6 14 85 5 104 84 9 14 85 5 103 83 6 107 86 12 56 12 18 106 13 4 106 13 4 14 85 5 107 86 12 108 87 11 107 86 12 14 85 5 14 85 5 104 84 9 108 87 11 62 67 29 108 87 11 104 84 9 107 86 6 109 88 3 56 12 8 17 90 5 110 89 4 109 88 3 109 88 3 107 86 6 17 90 5 108 87 9 17 90 5 107 86 6 93 70 12 61 73 18 110 89 4 110 89 4 17 90 5 93 70 12 94 68 11 93 70 12 17 90 5 17 90 5 108 87 9 94 68 11 62 67 29 94 68 11 108 87 9 109 88 6 111 29 3 56 12 8 18 91 5 112 26 4 111 29 3 111 29 3 109 88 6 18 91 5 110 89 9 18 91 5 109 88 6 113 92 12 57 25 18 112 26 4 112 26 4 18 91 5 113 92 12 114 93 11 113 92 12 18 91 5 18 91 5 110 89 9 114 93 11 61 73 29 114 93 11 110 89 9 113 92 6 89 58 3 57 25 8 28 94 5 90 60 4 89 58 3 89 58 3 113 92 6 28 94 5 114 93 9 28 94 5 113 92 6 115 95 12 58 63 18 90 60 4 90 60 4 28 94 5 115 95 12 116 96 11 115 95 12 28 94 5 28 94 5 114 93 9 116 96 11 61 73 29 116 96 11 114 93 9 115 95 6 118 97 3 58 63 8 31 99 5 117 98 4 118 97 3 118 97 3 115 95 6 31 99 5 116 96 9 31 99 5 115 95 6 95 75 12 52 76 18 117 98 4 117 98 4 31 99 5 95 75 12 96 74 11 95 75 12 31 99 5 31 99 5 116 96 9 96 74 11 61 73 29 96 74 11 116 96 9 120 100 6 117 98 3 52 76 8 32 101 5 118 97 4 117 98 3 117 98 3 120 100 6 32 101 5 119 17 9 32 101 5 120 100 6 92 66 12 58 63 18 118 97 4 118 97 4 32 101 5 92 66 12 91 65 11 92 66 12 32 101 5 32 101 5 119 17 9 91 65 11 51 18 29 91 65 11 119 17 9 72 43 6 102 78 3 53 42 8 33 102 5 101 81 4 102 78 3 102 78 3 72 43 6 33 102 5 71 16 9 33 102 5 72 43 6 120 100 12 52 76 18 101 81 4 101 81 4 33 102 5 120 100 12 119 17 11 120 100 12 33 102 5 33 102 5 71 16 9 119 17 11 250 106 4 133 105 5 249 104 3 208 107 6 249 104 3 133 105 5 133 105 5 207 110 9 208 107 6 138 103 7 153 103 17 175 103 16 184 113 18 234 112 12 250 106 4 163 103 19 149 103 20 162 103 13 150 103 27 176 103 28 154 103 26 247 10 11 179 114 29 199 15 9 169 103 22 155 103 31 178 103 30 133 105 5 250 106 4 234 112 12 234 112 12 233 115 11 133 105 5 207 110 9 133 105 5 233 115 11 233 115 11 182 116 29 207 110 9 213 118 3 249 104 6 183 117 8 146 103 25 150 103 27 154 103 26 214 120 4 145 119 5 213 118 3 249 104 6 213 118 3 145 119 5 145 119 5 250 106 9 249 104 6 185 122 18 240 121 12 214 120 4 145 119 5 214 120 4 240 121 12 240 121 12 239 124 11 145 119 5 250 106 9 145 119 5 239 124 11 151 103 7 138 103 7 175 103 16 239 124 11 184 113 29 250 106 9 177 103 10 163 103 19 162 103 13 155 103 31 158 103 46 178 103 30 249 104 3 208 107 6 183 117 8 192 127 3 196 126 6 188 125 8 191 129 4 251 128 5 192 127 3 196 126 6 192 127 3 251 128 5 251 128 5 195 130 9 196 126 6 187 132 18 194 131 12 191 129 4 251 128 5 191 129 4 194 131 12 194 131 12 193 133 11 251 128 5 195 130 9 251 128 5 193 133 11 193 133 11 179 114 29 195 130 9 198 134 3 194 131 6 187 132 8 197 135 4 252 51 5 198 134 3 194 131 6 198 134 3 252 51 5 252 51 5 193 133 9 194 131 6 181 8 18 200 7 12 197 135 4 252 51 5 197 135 4 200 7 12 200 7 12 199 15 11 252 51 5 193 133 9 252 51 5 199 15 11 199 15 11 179 114 29 193 133 9 202 136 3 198 134 6 187 132 8 201 138 4 253 137 5 202 136 3 198 134 6 202 136 3 253 137 5 253 137 5 197 135 9 198 134 6 182 116 18 204 139 12 201 138 4 253 137 5 201 138 4 204 139 12 204 139 12 203 140 11 253 137 5 197 135 9 253 137 5 203 140 11 203 140 11 181 8 29 197 135 9 206 141 3 202 136 6 187 132 8 205 143 4 254 142 5 206 141 3 202 136 6 206 141 3 254 142 5 254 142 5 201 138 9 202 136 6 183 117 18 208 107 12 205 143 4 254 142 5 205 143 4 208 107 12 208 107 12 207 110 11 254 142 5 201 138 9 254 142 5 207 110 11 207 110 11 182 116 29 201 138 9 191 129 3 206 141 6 187 132 8 192 127 4 255 144 5 191 129 3 206 141 6 191 129 3 255 144 5 255 144 5 205 143 9 206 141 6 188 125 18 210 145 12 192 127 4 255 144 5 192 127 4 210 145 12 210 145 12 209 146 11 255 144 5 205 143 9 255 144 5 209 146 11 209 146 11 183 117 29 205 143 9 209 146 3 213 118 6 183 117 8 210 145 4 0 147 5 209 146 3 213 118 6 209 146 3 0 147 5 0 147 5 214 120 9 213 118 6 188 125 18 212 148 12 210 145 4 0 147 5 210 145 4 212 148 12 212 148 12 211 149 11 0 147 5 214 120 9 0 147 5 211 149 11 211 149 11 185 122 29 214 120 9 211 149 3 217 150 6 185 122 8 212 148 4 20 151 5 211 149 3 217 150 6 211 149 3 20 151 5 20 151 5 218 152 9 217 150 6 188 125 18 216 153 12 212 148 4 20 151 5 212 148 4 216 153 12 216 153 12 215 154 11 20 151 5 218 152 9 20 151 5 215 154 11 215 154 11 186 155 29 218 152 9 196 126 3 216 153 6 188 125 8 195 130 4 29 156 5 196 126 3 216 153 6 196 126 3 29 156 5 29 156 5 215 154 9 216 153 6 179 114 18 219 157 12 195 130 4 29 156 5 195 130 4 219 157 12 219 157 12 220 158 11 29 156 5 215 154 9 29 156 5 220 158 11 220 158 11 186 155 29 215 154 9 222 161 3 226 160 6 190 159 8 221 163 4 3 162 5 222 161 3 226 160 6 222 161 3 3 162 5 3 162 5 225 164 9 226 160 6 189 166 18 224 165 12 221 163 4 3 162 5 221 163 4 224 165 12 224 165 12 223 167 11 3 162 5 225 164 9 3 162 5 223 167 11 223 167 11 180 28 29 225 164 9 227 168 3 230 6 6 181 8 8 228 169 4 140 85 5 227 168 3 230 6 6 227 168 3 140 85 5 140 85 5 229 14 9 230 6 6 190 159 18 226 160 12 228 169 4 140 85 5 228 169 4 226 160 12 226 160 12 225 164 11 140 85 5 229 14 9 140 85 5 225 164 11 225 164 11 180 28 29 229 14 9 203 140 3 227 168 6 181 8 8 204 139 4 143 170 5 203 140 3 227 168 6 203 140 3 143 170 5 143 170 5 228 169 9 227 168 6 182 116 18 231 171 12 204 139 4 143 170 5 204 139 4 231 171 12 231 171 12 232 172 11 143 170 5 228 169 9 143 170 5 232 172 11 232 172 11 190 159 29 228 169 9 233 115 3 231 171 6 182 116 8 234 112 4 144 173 5 233 115 3 231 171 6 233 115 3 144 173 5 144 173 5 232 172 9 231 171 6 184 113 18 235 174 12 234 112 4 144 173 5 234 112 4 235 174 12 235 174 12 236 175 11 144 173 5 232 172 9 144 173 5 236 175 11 236 175 11 190 159 29 232 172 9 237 176 3 235 174 6 184 113 8 238 178 4 147 177 5 237 176 3 235 174 6 237 176 3 147 177 5 147 177 5 236 175 9 235 174 6 189 166 18 221 163 12 238 178 4 147 177 5 238 178 4 221 163 12 221 163 12 222 161 11 147 177 5 236 175 9 147 177 5 222 161 11 222 161 11 190 159 29 236 175 9 239 124 3 237 176 6 184 113 8 240 121 4 148 99 5 239 124 3 237 176 6 239 124 3 148 99 5 148 99 5 238 178 9 237 176 6 185 122 18 241 179 12 240 121 4 148 99 5 240 121 4 241 179 12 241 179 12 242 180 11 148 99 5 238 178 9 148 99 5 242 180 11 242 180 11 189 166 29 238 178 9 217 150 3 241 179 6 185 122 8 218 152 4 157 181 5 217 150 3 241 179 6 217 150 3 157 181 5 157 181 5 242 180 9 241 179 6 186 155 18 243 182 12 218 152 4 157 181 5 218 152 4 243 182 12 243 182 12 244 183 11 157 181 5 242 180 9 157 181 5 244 183 11 244 183 11 189 166 29 242 180 9 246 184 3 243 182 6 186 155 8 245 186 4 159 185 5 246 184 3 243 182 6 246 184 3 159 185 5 159 185 5 244 183 9 243 182 6 180 28 18 223 167 12 245 186 4 159 185 5 245 186 4 223 167 12 223 167 12 224 165 11 159 185 5 244 183 9 159 185 5 224 165 11 224 165 11 189 166 29 244 183 9 245 186 3 248 11 6 180 28 8 246 184 4 160 187 5 245 186 3 248 11 6 245 186 3 160 187 5 160 187 5 247 10 9 248 11 6 186 155 18 220 158 12 246 184 4 160 187 5 246 184 4 220 158 12 220 158 12 219 157 11 160 187 5 247 10 9 160 187 5 219 157 11 219 157 11 179 114 29 247 10 9

0 0 -0 0 1 0 -0 1 0 0 0 0 0 1 -0 1 1 1
================================================ FILE: static/models/headset.dae ================================================ CINEMA4D 16.050 COLLADA Exporter 2017-02-21T21:06:11Z 2017-02-21T21:06:11Z Y_UP 0.8 0.8 0.8 1 0.2 0.2 0.2 1 0.5 0.8 0.8 0.8 1 0.2 0.2 0.2 1 0.5 -2.9479 0.8165 1.3314 -3.1091 -0.0978 1.3314 -3.4426 -0.6754 1.3314 -3.5674 -0.8789 1.3314 -4.25 -1.4517 1.3314 -4.5394 -1.557 1.3314 -5.1167 -1.7672 1.3314 -6.0666 -1.7672 1.3314 -6.9141 -1.4587 1.3314 -7.605 -0.8789 1.3314 -8.056 -0.0978 1.3314 -8.2172 0.8165 1.3314 -8.056 1.7309 1.3314 -7.605 2.512 1.3314 -6.9141 3.0917 1.3314 -6.0666 3.4002 1.3314 -5.1167 3.4002 1.3314 -4.5394 3.19 1.3314 -4.25 3.0847 1.3314 -3.5674 2.512 1.3314 -3.4426 2.3085 1.3314 -3.1091 1.7309 1.3314 4.5392 -1.5569 1.3314 4.2502 -1.4517 1.3314 3.5676 -0.8789 1.3314 3.4427 -0.6754 1.3314 3.1093 -0.0978 1.3314 2.9481 0.8165 1.3314 3.1093 1.7309 1.3314 3.4428 2.3085 1.3314 3.5676 2.512 1.3314 4.2502 3.0847 1.3314 4.5392 3.1899 1.3314 5.1169 3.4002 1.3314 6.0668 3.4002 1.3314 6.9143 3.0917 1.3314 7.6052 2.512 1.3314 8.0562 1.7309 1.3314 8.2174 0.8165 1.3314 8.0562 -0.0978 1.3314 7.6052 -0.8789 1.3314 6.9143 -1.4587 1.3314 6.0668 -1.7672 1.3314 5.1169 -1.7672 1.3314 -3.3316 -5.69354 12.5911 -3.80335 -6.38427 12.5911 -4.67863 -6.65578 12.5911 -9.11033 -6.54428 12.5911 -9.55339 -6.34912 12.5911 -10.0438 -5.9417 12.5911 -10.3654 -5.49723 12.5911 -10.5991 -4.99873 12.5911 -10.6554 -4.45481 12.5911 -10.6554 4.46366 12.5911 -10.5989 5.01576 12.5911 -10.3654 5.51358 12.5911 -10.0438 5.95779 12.5911 -9.55332 6.36563 12.5911 -9.12258 6.5551 12.5911 9.12246 6.5551 12.5911 9.55365 6.3655 12.5911 10.0443 5.95788 12.5911 10.3542 5.52931 12.5911 10.576 5.01375 12.5911 10.6551 4.44546 12.5911 10.6551 -4.43648 12.5911 10.5764 -4.99651 12.5911 10.3542 -5.51301 12.5911 10.0443 -5.94171 12.5911 9.55372 -6.349 12.5911 9.1102 -6.54428 12.5911 4.67851 -6.65578 12.5911 3.80347 -6.38418 12.5911 3.36388 -6.16999 12.5911 2.823 -5.45925 12.5911 2.08924 -3.73983 12.5911 1.21424 -2.68898 12.5911 -4.06528e-05 -2.35788 12.5911 -1.21401 -2.689 12.5911 -2.08916 -3.73981 12.5911 -2.8233 -5.4593 12.5911 -3.83761 -6.10865 12.5911 -2.712 -6.9648 1.47523 -1.9523 -5.966 1.47523 -1.227 -4.2672 1.47523 -0.6471 -3.5709 1.47523 0 -3.3944 1.47523 0.6473 -3.5709 1.47523 1.227 -4.2671 1.47523 1.952 -5.966 1.47523 2.7121 -6.9648 1.47523 3.4341 -7.3166 1.47523 4.5392 -7.6596 1.47523 9.3326 -7.539 1.47523 10.0848 -7.2078 1.47523 10.7805 -6.6302 1.47523 11.2292 -6.0095 1.47523 11.548 -5.2685 1.47523 11.6551 -4.5064 1.47523 11.6551 4.5147 1.47523 11.5479 5.2851 1.47523 11.2292 6.0259 1.47523 10.7805 6.6463 1.47523 10.0847 7.2244 1.47523 9.3326 7.5551 1.47523 -9.3328 7.5551 1.47523 -10.0846 7.2244 1.47523 -10.78 6.6462 1.47523 -11.2307 6.0236 1.47523 -11.5764 5.2866 1.47523 -11.6554 4.5147 1.47523 -11.6554 -4.5064 1.47523 -11.5764 -5.27 1.47523 -11.2308 -6.0071 1.47523 -10.7799 -6.6302 1.47523 -10.0847 -7.2078 1.47523 -9.3328 -7.539 1.47523 -4.5394 -7.6596 1.47523 -3.434 -7.3167 1.47523 -2.712 -6.9648 12.4752 -1.9523 -5.966 12.4752 -1.227 -4.2672 12.4752 -0.6471 -3.5709 12.4752 0 -3.3944 12.4752 0.6473 -3.5709 12.4752 1.227 -4.2671 12.4752 1.952 -5.966 12.4752 2.7121 -6.9648 12.4752 3.4341 -7.3166 12.4752 4.5392 -7.6596 12.4752 9.3326 -7.539 12.4752 10.0848 -7.2078 12.4752 10.7805 -6.6302 12.4752 11.2292 -6.0095 12.4752 11.548 -5.2685 12.4752 11.6551 -4.5064 12.4752 11.6551 4.5147 12.4752 11.5479 5.2851 12.4752 11.2292 6.0259 12.4752 10.7805 6.6463 12.4752 10.0847 7.2244 12.4752 9.3326 7.5551 12.4752 -9.3328 7.5551 12.4752 -10.0846 7.2244 12.4752 -10.78 6.6462 12.4752 -11.2307 6.0236 12.4752 -11.5764 5.2866 12.4752 -11.6554 4.5147 12.4752 -11.6554 -4.5064 12.4752 -11.5764 -5.27 12.4752 -11.2308 -6.0071 12.4752 -10.7799 -6.6302 12.4752 -10.0847 -7.2078 12.4752 -9.3328 -7.539 12.4752 -4.5394 -7.6596 12.4752 -3.434 -7.3167 12.4752 -2.712 -6.9648 1.47523 -1.9523 -5.966 1.47523 -1.227 -4.2672 1.47523 -0.6471 -3.5709 1.47523 0 -3.3944 1.47523 0.6473 -3.5709 1.47523 1.227 -4.2671 1.47523 1.952 -5.966 1.47523 2.7121 -6.9648 1.47523 3.4341 -7.3166 1.47523 4.5392 -7.6596 1.47523 9.3326 -7.539 1.47523 10.0848 -7.2078 1.47523 10.7805 -6.6302 1.47523 11.2292 -6.0095 1.47523 11.548 -5.2685 1.47523 11.6551 -4.5064 1.47523 11.6551 4.5147 1.47523 11.5479 5.2851 1.47523 11.2292 6.0259 1.47523 10.7805 6.6463 1.47523 10.0847 7.2244 1.47523 9.3326 7.5551 1.47523 -9.3328 7.5551 1.47523 -10.0846 7.2244 1.47523 -10.78 6.6462 1.47523 -11.2307 6.0236 1.47523 -11.5764 5.2866 1.47523 -11.6554 4.5147 1.47523 -11.6554 -4.5064 1.47523 -11.5764 -5.27 1.47523 -11.2308 -6.0071 1.47523 -10.7799 -6.6302 1.47523 -10.0847 -7.2078 1.47523 -9.3328 -7.539 1.47523 -4.5394 -7.6596 1.47523 -3.434 -7.3167 1.47523 -2.712 -6.9648 12.4752 -1.9523 -5.966 12.4752 -1.227 -4.2672 12.4752 -0.6471 -3.5709 12.4752 0 -3.3944 12.4752 0.6473 -3.5709 12.4752 1.227 -4.2671 12.4752 1.952 -5.966 12.4752 2.7121 -6.9648 12.4752 3.4341 -7.3166 12.4752 4.5392 -7.6596 12.4752 9.3326 -7.539 12.4752 10.0848 -7.2078 12.4752 10.7805 -6.6302 12.4752 11.2292 -6.0095 12.4752 11.548 -5.2685 12.4752 11.6551 -4.5064 12.4752 11.6551 4.5147 12.4752 11.5479 5.2851 12.4752 11.2292 6.0259 12.4752 10.7805 6.6463 12.4752 10.0847 7.2244 12.4752 9.3326 7.5551 12.4752 -9.3328 7.5551 12.4752 -10.0846 7.2244 12.4752 -10.78 6.6462 12.4752 -11.2307 6.0236 12.4752 -11.5764 5.2866 12.4752 -11.6554 4.5147 12.4752 -11.6554 -4.5064 12.4752 -11.5764 -5.27 12.4752 -11.2308 -6.0071 12.4752 -10.7799 -6.6302 12.4752 -10.0847 -7.2078 12.4752 -9.3328 -7.539 12.4752 -4.5394 -7.6596 12.4752 -3.434 -7.3167 12.4752 0 0 -1 0 0 1 0.63424 -0.773136 -0 0.864375 -0.502847 -0 0.853057 -0.521818 -0 0.54074 -0.84119 -0 3.92157e-05 -1 -0 -0.540749 -0.841184 -0 -0.853127 -0.521703 -0 -0.864355 -0.502883 -0 -0.634099 -0.773252 -0 -0.368297 -0.929708 -0 -0.137462 -0.990507 -0 0.21819 -0.975906 -0 0.525956 -0.850512 -0 0.730389 -0.683032 -0 0.869746 -0.4935 -0 0.962979 -0.269578 -0 0.997564 -0.0697522 -0 0.997611 0.0690751 -0 0.963163 0.268917 -0 0.869693 0.493592 -0 0.730431 0.682986 -0 0.52589 0.850552 -0 0.205651 0.978625 -0 -0.205723 0.97861 -0 -0.526109 0.850417 -0 -0.73041 0.683009 -0 -0.861495 0.507767 -0 -0.963693 0.267013 -0 -0.9987 0.0509731 -0 -0.998672 -0.0515228 -0 -0.963568 -0.267465 -0 -0.861579 -0.507623 -0 -0.730342 -0.683082 -0 -0.526169 -0.85038 -0 -0.218262 -0.97589 -0 0.137383 -0.990518 -0 0.368275 -0.929717 -0 0.290521 0.933573 0.290521 0.752011 0.310814 0.787156 0.320622 0.842789 0.310814 0.898428 0.223784 0.698368 0.241393 0.704775 0.282927 0.739629 0.223784 0.98721 0.07929 0.704349 0.130858 0.685578 0.188657 0.685578 0.00980857 0.787156 0.0372507 0.739629 0 0.842789 0.0372507 0.945955 0.00980857 0.898428 0.130858 1 0.07929 0.981229 0.188657 1 0.282927 0.945955 0.241393 0.980803 1 0.842789 0.776192 0.987204 0.709479 0.933573 0.776192 0.698374 0.962749 0.739629 0.990191 0.787156 0.869142 0.685578 0.92071 0.704349 0.811343 0.685578 0.709473 0.752011 0.717073 0.739629 0.758607 0.704775 0.679378 0.842789 0.689186 0.787156 0.689186 0.898428 0.758607 0.980803 0.717073 0.945955 0.92071 0.981229 0.869142 1 0.811343 1 0.990191 0.898428 0.962749 0.945955 0.985881 0.951865 0.996289 0.927672 0.971336 0.971975 1 0.901005 0.948314 0.991103 0.92808 1 1 0.484218 0.996307 0.457938 0.985879 0.433701 0.971337 0.413584 0.948317 0.394472 0.927505 0.385309 0.719547 0.380077 0.678486 0.392821 0.657858 0.402872 0.598045 0.516908 0.556985 0.56622 0.500005 0.581756 0.443039 0.566219 0.0719279 1 0.401973 0.516909 0.367523 0.436221 0.0517153 0.991109 0.319926 0.405751 0.343671 0.42523 0.321534 0.392817 0.0286985 0.971971 0.013609 0.951127 0.280461 0.380077 0.00265149 0.927766 0 0.901859 0.0725029 0.385309 0.0517119 0.394467 0 0.483358 0.0287012 0.413585 0.00264061 0.457834 0.0136083 0.434442 0.632477 0.436224 0 1 0 0 0.0165375 0 0.0165375 1 0.0408802 0 0.0408802 1 0.0528219 0 0.0528219 1 0.0616612 0 0.0616612 1 0.0705031 0 0.0705031 1 0.0824421 0 0.0824421 1 0.106784 0 0.106784 1 0.123325 0 0.123325 1 0.133909 0 0.133909 1 0.149158 0 0.149158 1 0.212348 0 0.212348 1 0.223179 0 0.223179 1 0.235095 0 0.235095 1 0.245189 0 0.245189 1 0.255819 0 0.255819 1 0.265961 0 0.265961 1 0.384846 0 0.384846 1 0.395096 0 0.395096 1 0.405724 0 0.405724 1 0.415814 0 0.415814 1 0.427736 0 0.427736 1 0.438563 0 0.438563 1 0.684544 0 0.684544 1 0.695368 0 0.695368 1 0.707286 0 0.707286 1 0.717415 0 0.717415 1 0.728143 0 0.728143 1 0.738369 0 0.738369 1 0.857253 0 0.857253 1 0.86737 0 0.86737 1 0.878098 0 0.878098 1 0.888234 0 0.888234 1 0.900146 0 0.900146 1 0.910973 0 0.910973 1 0.974163 0 0.974163 1 0.989415 0 0.989415 1 1 0 1 1 0.383664 0.377109 0.416254 0.419957 0.352691 0.362013 0.447369 0.492834 0.30527 0.347303 0.472246 0.522704 0 0.86957 0 0.482572 0.500006 0.530276 0.0996375 1 0.527775 0.522704 0.900367 1 0.552644 0.492838 1 0.86957 0.583746 0.419957 0.694734 0.347303 1 0.482572 0.616353 0.377109 0.647326 0.362017 0.900367 0.352476 0.932635 0.366685 0.96248 0.391463 0.981729 0.418091 0.995405 0.449879 0.995401 0.902619 0.981729 0.934399 0.96248 0.961013 0.932631 0.985813 0.0673859 0.985813 0.0375539 0.961009 0.0182193 0.9343 0.00338903 0.902683 0.00338903 0.449814 0.018215 0.418194 0.0996375 0.352476 0.0375582 0.391463 0.0673817 0.366685

1 0 2 2 0 1 20 0 0 0 0 3 1 0 2 20 0 0 0 0 3 20 0 0 21 0 4 3 0 7 4 0 6 5 0 5 2 0 1 3 0 7 5 0 5 5 0 5 17 0 8 20 0 0 2 0 1 5 0 5 20 0 0 6 0 11 7 0 10 8 0 9 5 0 5 6 0 11 8 0 9 8 0 9 9 0 13 10 0 12 5 0 5 8 0 9 10 0 12 10 0 12 11 0 14 17 0 8 5 0 5 10 0 12 17 0 8 12 0 16 13 0 15 17 0 8 11 0 14 12 0 16 17 0 8 14 0 18 15 0 17 17 0 8 13 0 15 14 0 18 17 0 8 15 0 17 16 0 19 17 0 8 18 0 21 19 0 20 20 0 0 17 0 8 18 0 21 20 0 0 29 0 24 32 0 23 38 0 22 22 0 25 29 0 24 38 0 22 38 0 22 39 0 27 40 0 26 22 0 25 38 0 22 40 0 26 40 0 26 41 0 29 42 0 28 22 0 25 40 0 26 42 0 28 22 0 25 42 0 28 43 0 30 23 0 33 24 0 32 25 0 31 22 0 25 23 0 33 25 0 31 25 0 31 27 0 34 29 0 24 22 0 25 25 0 31 29 0 24 25 0 31 26 0 35 27 0 34 27 0 34 28 0 36 29 0 24 30 0 38 31 0 37 32 0 23 29 0 24 30 0 38 32 0 23 33 0 41 34 0 40 35 0 39 32 0 23 33 0 41 35 0 39 35 0 39 36 0 43 37 0 42 32 0 23 35 0 39 37 0 42 32 0 23 37 0 42 38 0 22 61 1 46 63 1 45 62 1 44 61 1 46 64 1 47 63 1 45 60 1 48 64 1 47 61 1 46 60 1 48 59 1 49 64 1 47 59 1 49 65 1 50 64 1 47 66 1 51 65 1 50 59 1 49 67 1 52 66 1 51 59 1 49 68 1 53 67 1 52 59 1 49 69 1 54 68 1 53 59 1 49 70 1 55 69 1 54 59 1 49 71 1 56 70 1 55 59 1 49 72 1 57 71 1 56 59 1 49 73 1 58 72 1 57 59 1 49 75 1 59 73 1 58 59 1 49 76 1 60 75 1 59 59 1 49 77 1 61 76 1 60 59 1 49 78 1 62 77 1 61 59 1 49 58 1 63 78 1 62 59 1 49 58 1 63 79 1 64 78 1 62 58 1 63 80 1 65 79 1 64 58 1 63 57 1 66 80 1 65 57 1 66 81 1 67 80 1 65 45 1 69 44 1 68 81 1 67 45 1 69 81 1 67 57 1 66 56 1 70 45 1 69 57 1 66 56 1 70 55 1 71 45 1 69 55 1 71 46 1 72 45 1 69 54 1 73 46 1 72 55 1 71 54 1 73 53 1 74 46 1 72 53 1 74 47 1 75 46 1 72 48 1 76 47 1 75 53 1 74 52 1 77 48 1 76 53 1 74 52 1 77 49 1 78 48 1 76 51 1 79 49 1 78 52 1 77 51 1 79 50 1 80 49 1 78 75 1 59 74 1 81 73 1 58

120 3 84 119 2 83 82 2 82 83 3 85 120 3 84 82 2 82 121 4 86 120 3 84 83 3 85 84 4 87 121 4 86 83 3 85 122 5 88 121 4 86 84 4 87 85 5 89 122 5 88 84 4 87 123 6 90 122 5 88 85 5 89 86 6 91 123 6 90 85 5 89 124 7 92 123 6 90 86 6 91 87 7 93 124 7 92 86 6 91 125 8 94 124 7 92 87 7 93 88 8 95 125 8 94 87 7 93 126 9 96 125 8 94 88 8 95 89 9 97 126 9 96 88 8 95 127 10 98 126 9 96 89 9 97 90 10 99 127 10 98 89 9 97 128 11 100 127 10 98 90 10 99 91 11 101 128 11 100 90 10 99 129 12 102 128 11 100 91 11 101 92 12 103 129 12 102 91 11 101 130 13 104 129 12 102 92 12 103 93 13 105 130 13 104 92 12 103 131 14 106 130 13 104 93 13 105 94 14 107 131 14 106 93 13 105 132 15 108 131 14 106 94 14 107 95 15 109 132 15 108 94 14 107 133 16 110 132 15 108 95 15 109 96 16 111 133 16 110 95 15 109 134 17 112 133 16 110 96 16 111 97 17 113 134 17 112 96 16 111 135 18 114 134 17 112 97 17 113 98 18 115 135 18 114 97 17 113 136 19 116 135 18 114 98 18 115 99 19 117 136 19 116 98 18 115 137 20 118 136 19 116 99 19 117 100 20 119 137 20 118 99 19 117 138 21 120 137 20 118 100 20 119 101 21 121 138 21 120 100 20 119 139 22 122 138 21 120 101 21 121 102 22 123 139 22 122 101 21 121 140 23 124 139 22 122 102 22 123 103 23 125 140 23 124 102 22 123 141 24 126 140 23 124 103 23 125 104 24 127 141 24 126 103 23 125 142 25 128 141 24 126 104 24 127 105 25 129 142 25 128 104 24 127 143 26 130 142 25 128 105 25 129 106 26 131 143 26 130 105 25 129 144 27 132 143 26 130 106 26 131 107 27 133 144 27 132 106 26 131 145 28 134 144 27 132 107 27 133 108 28 135 145 28 134 107 27 133 146 29 136 145 28 134 108 28 135 109 29 137 146 29 136 108 28 135 147 30 138 146 29 136 109 29 137 110 30 139 147 30 138 109 29 137 148 31 140 147 30 138 110 30 139 111 31 141 148 31 140 110 30 139 149 32 142 148 31 140 111 31 141 112 32 143 149 32 142 111 31 141 150 33 144 149 32 142 112 32 143 113 33 145 150 33 144 112 32 143 151 34 146 150 33 144 113 33 145 114 34 147 151 34 146 113 33 145 152 35 148 151 34 146 114 34 147 115 35 149 152 35 148 114 34 147 153 36 150 152 35 148 115 35 149 116 36 151 153 36 150 115 35 149 154 37 152 153 36 150 116 36 151 117 37 153 154 37 152 116 36 151 155 38 154 154 37 152 117 37 153 118 38 155 155 38 154 117 37 153 119 2 156 155 38 154 118 38 155 82 2 157 119 2 156 118 38 155 192 0 160 157 0 159 156 0 158 191 0 162 158 0 161 157 0 159 192 0 160 191 0 162 157 0 159 184 0 164 159 0 163 158 0 161 185 0 165 184 0 164 158 0 161 191 0 162 185 0 165 158 0 161 179 0 167 160 0 166 159 0 163 184 0 164 179 0 167 159 0 163 178 0 169 161 0 168 160 0 166 179 0 167 178 0 169 160 0 166 173 0 171 162 0 170 161 0 168 178 0 169 173 0 171 161 0 168 166 0 173 163 0 172 162 0 170 172 0 174 166 0 173 162 0 170 173 0 171 172 0 174 162 0 170 165 0 176 164 0 175 163 0 172 166 0 173 165 0 176 163 0 172 172 0 174 167 0 177 166 0 173 169 0 179 168 0 178 167 0 177 170 0 180 169 0 179 167 0 177 172 0 174 170 0 180 167 0 177 172 0 174 171 0 181 170 0 180 175 0 183 174 0 182 173 0 171 178 0 169 175 0 183 173 0 171 178 0 169 176 0 184 175 0 183 178 0 169 177 0 185 176 0 184 181 0 187 180 0 186 179 0 167 182 0 188 181 0 187 179 0 167 184 0 164 182 0 188 179 0 167 184 0 164 183 0 189 182 0 188 187 0 191 186 0 190 185 0 165 190 0 192 187 0 191 185 0 165 191 0 162 190 0 192 185 0 165 190 0 192 188 0 193 187 0 191 190 0 192 189 0 194 188 0 193 193 1 158 194 1 159 229 1 160 194 1 159 228 1 162 229 1 160 194 1 159 195 1 161 228 1 162 222 1 165 227 1 192 228 1 162 195 1 161 222 1 165 228 1 162 225 1 193 226 1 194 227 1 192 224 1 191 225 1 193 227 1 192 222 1 165 224 1 191 227 1 192 222 1 165 223 1 190 224 1 191 195 1 161 221 1 164 222 1 165 219 1 188 220 1 189 221 1 164 216 1 167 219 1 188 221 1 164 196 1 163 216 1 167 221 1 164 195 1 161 196 1 163 221 1 164 216 1 167 218 1 187 219 1 188 216 1 167 217 1 186 218 1 187 197 1 166 215 1 169 216 1 167 196 1 163 197 1 166 216 1 167 213 1 184 214 1 185 215 1 169 212 1 183 213 1 184 215 1 169 210 1 171 212 1 183 215 1 169 198 1 168 210 1 171 215 1 169 197 1 166 198 1 168 215 1 169 210 1 171 211 1 182 212 1 183 199 1 170 209 1 174 210 1 171 198 1 168 199 1 170 210 1 171 207 1 180 208 1 181 209 1 174 204 1 177 207 1 180 209 1 174 203 1 173 204 1 177 209 1 174 199 1 170 203 1 173 209 1 174 204 1 177 206 1 179 207 1 180 204 1 177 205 1 178 206 1 179 200 1 172 202 1 176 203 1 173 199 1 170 200 1 172 203 1 173 200 1 172 201 1 175 202 1 176

0 0 -0.14383 0 1 0 -0 1 0 0 0 0 0 1 -0 1 1 1
================================================ FILE: static/models/stump-shadow.obj ================================================ # WaveFront *.obj file (generated by CINEMA 4D) mtllib ./shadowUpdated.mtl v 0.14950437671081 0.05640361318484 0.29568244315973 v 0.16634495138771 0.05640361318484 0.29966922580218 v 0.17401577298040 0.05640361318484 0.30970348700778 v 0.18462235904675 0.05640361318484 0.32149329583816 v 0.19553021914222 0.05640361318484 0.33854293906683 v 0.20410491305365 0.05640361318484 0.36435680580983 v 0.21336806422455 0.05640361318484 0.39501629619561 v 0.22634131183965 0.05640361318484 0.42660298042218 v 0.24049432598953 0.05640361318484 0.44988549773341 v 0.25329679350851 0.05640361318484 0.45563252148733 v 0.26018140789744 0.05640361318484 0.44498175983335 v 0.25658082967087 0.05640361318484 0.41907078471485 v 0.24995525947560 0.05640361318484 0.38590302570700 v 0.24776489921158 0.05640361318484 0.35348174244070 v 0.24454173579479 0.05640361318484 0.32297977584284 v 0.23481782562270 0.05640361318484 0.29556976340846 v 0.18211250219123 0.05640361318484 0.24747097659571 v 0.21515434871003 0.05640361318484 0.27061305324623 v 0.14856284193008 0.05640361318484 0.17312850561623 v 0.15385957358931 0.05640361318484 0.21776797891917 v 0.18699764575706 0.05640361318484 0.11381239081366 v 0.16126222205994 0.05640361318484 0.13224561920189 v 0.21963609506720 0.05640361318484 0.12842620998537 v 0.21128397448339 0.05640361318484 0.11386164162341 v 0.24790292128626 0.05640361318484 0.15105071379687 v 0.22689531420517 0.05640361318484 0.14499343891710 v 0.28987860703287 0.05640361318484 0.12421420123501 v 0.27233774324924 0.05640361318484 0.14474273458330 v 0.33150214087084 0.05640361318484 0.10563451652653 v 0.30733142426381 0.05640361318484 0.10574988540893 v 0.39886360438621 0.05640361318484 0.11864799747634 v 0.36210734842576 0.05640361318484 0.11341743487276 v 0.48592668714661 0.05640361318484 0.13332088169684 v 0.51072406454373 0.05640361318484 0.14050129945336 v 0.43458227458195 0.05640361318484 0.12286253247611 v 0.46207472465476 0.05640361318484 0.12759739350254 v 0.53823659546513 0.05640361318484 0.11908261821634 v 0.52050689407968 0.05640361318484 0.10924375479644 v 0.53089947328184 0.05640361318484 0.14252578095764 v 0.54088556403614 0.05640361318484 0.13278142665696 v 0.44789499677498 0.05640361318484 0.08032638312325 v 0.41614682010024 0.05640361318484 0.06342550464276 v 0.49604049800409 0.05640361318484 0.10100586282275 v 0.47318137751028 0.05640361318484 0.09210995122506 v 0.31934847687743 0.05640361318484 0.02672159860424 v 0.27280168335637 0.05640361318484 0.01624910670947 v 0.38296699750131 0.05640361318484 0.04676782253074 v 0.35338554300672 0.05640361318484 0.03571384678040 v 0.19347510574758 0.05640361318484 -0.04055761587822 v 0.17325003623261 0.05640361318484 -0.02420608073146 v 0.18790502168731 0.05640361318484 -0.00923874367124 v 0.22517669356847 0.05640361318484 0.00427083872904 v 0.35145694304190 0.05640361318484 -0.09285543299381 v 0.31091366180373 0.05640361318484 -0.07249108037442 v 0.27089562871957 0.05640361318484 -0.06246227608471 v 0.23166279348083 0.05640361318484 -0.05455559703281 v 0.47561869758021 0.05640361318484 -0.16272207486492 v 0.44676539566509 0.05640361318484 -0.14689830012785 v 0.41919371975148 0.05640361318484 -0.13232924816573 v 0.38879410261487 0.05640361318484 -0.11548994902276 v 0.46364446029141 0.05640361318484 -0.19350531332892 v 0.48937588920588 0.05640361318484 -0.19656096805127 v 0.50081694429232 0.05640361318484 -0.19021699447158 v 0.49666535072068 0.05640361318484 -0.17782137470253 v 0.27982034126556 0.05640361318484 -0.15720186474671 v 0.23544770015814 0.05640361318484 -0.16067550968258 v 0.19845218691849 0.05640361318484 -0.16578435686386 v 0.17441088604015 0.05640361318484 -0.16613172624464 v 0.43177421009452 0.05640361318484 -0.18576178973983 v 0.40191679312734 0.05640361318484 -0.17804215596742 v 0.36851032558320 0.05640361318484 -0.17011779400843 v 0.32599302624851 0.05640361318484 -0.16176017007986 v 0.15795525154965 0.05640361318484 -0.31309919685922 v 0.16318759201641 0.05640361318484 -0.34925876703950 v 0.15906958531543 0.05640361318484 -0.37770430091127 v 0.13687757954575 0.05640361318484 -0.39441981538226 v 0.15987238136593 0.05635788260275 -0.17303623915209 v 0.15138527448498 0.05640361318484 -0.19781661875459 v 0.14883232282003 0.05640361318484 -0.23353201378131 v 0.15209623281052 0.05640361318484 -0.27324157333737 v 0.05743107777568 0.05640361318484 -0.46751946434770 v 0.05773105358220 0.05640361318484 -0.45180773856575 v 0.05739863274067 0.05640361318484 -0.50828042799014 v 0.05691594347042 0.05640361318484 -0.48559278468791 v 0.08020460694005 0.05640361318484 -0.41296329284817 v 0.10709482730674 0.05640361318484 -0.40448092868382 v 0.05660289681164 0.05640361318484 -0.43620481089797 v 0.06158220869091 0.05640361318484 -0.42262018410592 v 0.03965969866836 0.05640361318484 -0.54706409382641 v 0.04310411245281 0.05640361318484 -0.55614585586501 v 0.03469332164653 0.05640361318484 -0.50740141882140 v 0.03778704956751 0.05640361318484 -0.53097392690294 v 0.05964233780276 0.05640361318484 -0.54562417817370 v 0.05845035741089 0.05640361318484 -0.53011376794331 v 0.05091312992534 0.05640361318484 -0.55869311533440 v 0.05809108896026 0.05640361318484 -0.55506589365342 v 0.00994350275504 0.05640361318484 -0.39507563872645 v 0.02178890365339 0.05640361318484 -0.38021270562588 v 0.01290828696189 0.05640361318484 -0.42578646389404 v 0.00909934840048 0.05640361318484 -0.41091999805923 v 0.01780949277752 0.05640361318484 -0.44955006372288 v 0.01502216456079 0.05640361318484 -0.43771568251186 v 0.03007755337493 0.05640361318484 -0.48292721212467 v 0.02363878241069 0.05640361318484 -0.46413198335231 v 0.00008827227549 0.05640361318484 -0.17909500313560 v -0.05458277635327 0.05640361318484 -0.16826089007699 v 0.03614902962933 0.05640361318484 -0.26638757727322 v 0.02429286215660 0.05640361318484 -0.21783528770653 v 0.06476393915451 0.05640361318484 -0.33663619044843 v 0.05377480878334 0.05640361318484 -0.30665768834030 v 0.03919223104353 0.05640361318484 -0.36655186224513 v 0.05671016121555 0.05640361318484 -0.35431363613598 v -0.24384573795526 0.05640361318484 -0.31510866209724 v -0.24184227348621 0.05640361318484 -0.29249216646681 v -0.22298667339906 0.05640361318484 -0.25543729288014 v -0.20038017448753 0.05640361318484 -0.22154342741369 v -0.18712414925012 0.05640361318484 -0.20840999113532 v -0.17393150962190 0.05640361318484 -0.20206046040144 v -0.15151520140146 0.05640361318484 -0.18851832881355 v -0.11376802191348 0.05640361318484 -0.17488473186326 v -0.26342857163534 0.05640361318484 -0.49849330228455 v -0.25352831296675 0.05640361318484 -0.49891639223142 v -0.24889724757534 0.05640361318484 -0.49344988389595 v -0.24279848106049 0.05640361318484 -0.47541081534760 v -0.22849525422505 0.05640361318484 -0.43811632762489 v -0.21810780774524 0.05640361318484 -0.39533861361764 v -0.22375644902185 0.05640361318484 -0.36085003666115 v -0.23561210552336 0.05640361318484 -0.33424266325032 v -0.30163916090096 0.05640361318484 -0.34020833534170 v -0.28306664180858 0.05640361318484 -0.35111954779776 v -0.34978939432746 0.05640361318484 -0.33010884170512 v -0.32498798462066 0.05640361318484 -0.33297553396816 v -0.38587666930047 0.05640361318484 -0.31606364184603 v -0.37157504736381 0.05640361318484 -0.32625573125835 v -0.40340609117008 0.05640361318484 -0.29575691538612 v -0.39553885326494 0.05640361318484 -0.30430619646312 v -0.27402172242684 0.05640361318484 -0.48676410197482 v -0.27234427867757 0.05640361318484 -0.49387703210881 v -0.26682249281253 0.05640361318484 -0.42754065799254 v -0.27125103683518 0.05640361318484 -0.46777764466091 v -0.26386820422688 0.05640361318484 -0.37854864797177 v -0.26345517517388 0.05640361318484 -0.38986158073516 v -0.27259404376657 0.05640361318484 -0.36502150654533 v -0.26720125681400 0.05640361318484 -0.37610193229430 v -0.48305609680417 0.05640361318484 -0.31347838429937 v -0.46681330835225 0.05640361318484 -0.28705572639149 v -0.44737600956643 0.05640361318484 -0.26613643333820 v -0.42464973953878 0.05640361318484 -0.26005494524050 v -0.39882082169765 0.05640361318484 -0.26389010350839 v -0.37007571592804 0.05640361318484 -0.27272068257666 v -0.34355822977156 0.05640361318484 -0.28012157090700 v -0.32441220475866 0.05640361318484 -0.27966765721171 v -0.41484951769907 0.05640361318484 -0.29560249728995 v -0.43524019980806 0.05640361318484 -0.30902960635937 v -0.45678659822136 0.05640361318484 -0.32936200077107 v -0.47169714017572 0.05640361318484 -0.34992337047342 v -0.48345430386632 0.05640361318484 -0.36369859647964 v -0.49554060197844 0.05640361318484 -0.36367249207593 v -0.50183055936458 0.05640361318484 -0.35330844175333 v -0.49619876885494 0.05640361318484 -0.33606983050415 v -0.27630975620629 0.05640361318484 -0.15257396878232 v -0.28394960410633 0.05640361318484 -0.18207085293964 v -0.29547847577751 0.05640361318484 -0.22525642544630 v -0.30944859139795 0.05640361318484 -0.26387417891985 v -0.20165659300473 0.05640361318484 -0.05108462792983 v -0.22809863364121 0.05640361318484 -0.08734120656978 v -0.26140930769094 0.05640361318484 -0.12493949018129 v -0.20736202759764 0.05640361318484 -0.02747528270583 v -0.22933636692715 0.05640361318484 -0.01274079767528 v -0.25170102351390 0.05640361318484 -0.00310879842822 v -0.26915086181584 0.05640361318484 -0.00651743500330 v -0.27638076325012 0.05640361318484 -0.03090486234418 v -0.28613649765992 0.05640361318484 -0.05594896295752 v -0.31116388588736 0.05640361318484 -0.06132761760160 v -0.34052379063217 0.05640361318484 -0.05754784770681 v -0.36327707461762 0.05640361318484 -0.05511667151706 v -0.38457342101879 0.05640361318484 -0.05323327214557 v -0.40956261503999 0.05640361318484 -0.05109682496023 v -0.43445280923376 0.05640361318484 -0.04917498305804 v -0.45545208815929 0.05640361318484 -0.04793540115903 v -0.47178926769648 0.05640361318484 -0.04689451313311 v -0.48269299381235 0.05640361318484 -0.04556874734843 v -0.49302679968208 0.05640361318484 -0.04296589966705 v -0.50765428641155 0.05640361318484 -0.03809377282519 v -0.52593973564569 0.05640361318484 -0.03220485891872 v -0.54724759893842 0.05640361318484 -0.02655165607632 v -0.56957123646536 0.05640361318484 -0.02146593120819 v -0.59090400837866 0.05640361318484 -0.01727945441095 v -0.60589162416557 0.05640361318484 -0.01088649680698 v -0.60917952138301 0.05640361318484 0.00081866983102 v -0.60204861310650 0.05640361318484 0.01065541308220 v -0.58578008427537 0.05640361318484 0.01144309214770 v -0.56434515541587 0.05640361318484 0.00795985488989 v -0.54171504705423 0.05640361318484 0.00498384917110 v -0.51980992053710 0.05640361318484 0.00288749407578 v -0.50054980120872 0.05640361318484 0.00204320331797 v -0.48504986931283 0.05640361318484 0.00151246011518 v -0.47442523706845 0.05640361318484 0.00035674181323 v -0.46370049829082 0.05640361318484 -0.00219620338477 v -0.44790028082317 0.05640361318484 -0.00691862221559 v -0.42741422271441 0.05640361318484 -0.01145466833510 v -0.40263189405930 0.05640361318484 -0.01344849171146 v -0.37764218672262 0.05640361318484 -0.01411035181807 v -0.35653395858818 0.05640361318484 -0.01465050681558 v -0.34605127454691 0.05640361318484 -0.00556339287801 v -0.35293813153558 0.05640361318484 0.02265655750833 v -0.35683276426059 0.05640361318484 0.05370360683366 v -0.33737337347081 0.05640361318484 0.07127202208745 v -0.31208130897664 0.05640361318484 0.08990899760028 v -0.29847795435798 0.05640361318484 0.12416169771185 v -0.29654684363749 0.05640361318484 0.15950347031736 v -0.30627161255372 0.05640361318484 0.18140762857124 v -0.32326777566889 0.05640361318484 0.18871370206802 v -0.34315074545337 0.05640361318484 0.18026120415970 v -0.36283891590967 0.05640361318484 0.17283220892213 v -0.37925071502913 0.05640361318484 0.18320879018052 v -0.39352632295631 0.05640361318484 0.20047502803526 v -0.40680588584693 0.05640361318484 0.21371500283735 v -0.42115711356509 0.05640361318484 0.22472940326308 v -0.43864781806677 0.05640361318484 0.23531893423126 v -0.45517281301009 0.05640361318484 0.24492664402451 v -0.46662694629266 0.05640361318484 0.25299561466362 v -0.47853605547785 0.05640361318484 0.25985829960536 v -0.49642604635734 0.05640361318484 0.26584718579406 v -0.51723246042089 0.05640361318484 0.27218931377132 v -0.53789070408005 0.05640361318484 0.28011184404220 v -0.55262593583353 0.05640361318484 0.28846343695847 v -0.55566331518256 0.05640361318484 0.29609288882726 v -0.54822396146570 0.05640361318484 0.29984314687433 v -0.53152913148069 0.05640361318484 0.29655736125599 v -0.51035571713400 0.05640361318484 0.29050526287923 v -0.48948061183591 0.05640361318484 0.28595678658414 v -0.47152479820250 0.05640361318484 0.28186476066797 v -0.45910936106706 0.05640361318484 0.27718204666495 v -0.44642716347938 0.05640361318484 0.27209810211571 v -0.42767103399915 0.05640361318484 0.26680231683382 v -0.40451291164032 0.05640361318484 0.25946994410446 v -0.37862476902979 0.05640361318484 0.24827618597890 v -0.35405842671150 0.05640361318484 0.24266975881972 v -0.33486573871698 0.05640361318484 0.25209931076122 v -0.32300667919377 0.05640361318484 0.26873818847084 v -0.32044118792458 0.05640361318484 0.28475968788339 v -0.32271866177141 0.05640361318484 0.30338162379047 v -0.32538846385804 0.05640361318484 0.32782184522314 v -0.33215983426196 0.05640361318484 0.35337934767842 v -0.34674204629763 0.05640361318484 0.37535302443612 v -0.35909828918493 0.05640361318484 0.39204138269945 v -0.35919185411029 0.05640361318484 0.40174292891971 v -0.34948329760540 0.05640361318484 0.40192757262150 v -0.33243331315988 0.05640361318484 0.39006535828229 v -0.31126691691729 0.05640361318484 0.36865641583427 v -0.28920929521606 0.05640361318484 0.34020090794535 v -0.27388342827381 0.05640361318484 0.30774423993483 v -0.27291222908236 0.05640361318484 0.27433191958983 v -0.27320387717831 0.05640361318484 0.24498169255796 v -0.26166653472789 0.05640361318484 0.22471125362889 v -0.24526690292632 0.05640361318484 0.20744551842185 v -0.23097171708298 0.05640361318484 0.18710941929987 v -0.22367700547229 0.05640361318484 0.16798176793520 v -0.22827888146611 0.05640361318484 0.15434139236796 v -0.23508394535485 0.05640361318484 0.14099087375839 v -0.23439881429798 0.05640361318484 0.12273277614699 v -0.21862695398085 0.05640361318484 0.10931104226703 v -0.18017176211105 0.05640361318484 0.11046961535304 v -0.14304502763687 0.05640361318484 0.12843447329074 v -0.13125852263746 0.05640361318484 0.16543161108559 v -0.13614725519273 0.05640361318484 0.21043284244991 v -0.14904616515427 0.05640361318484 0.25240994760846 v -0.16460171034997 0.05640361318484 0.29277241542558 v -0.17746031424294 0.05640361318484 0.33292968403301 v -0.19011308427649 0.05640361318484 0.37500639545230 v -0.20505107640939 0.05640361318484 0.42112712410328 v -0.22175703819869 0.05640361318484 0.46715262628915 v -0.23971364897313 0.05640361318484 0.50894362482552 v -0.24831379974442 0.05640361318484 0.54119825497631 v -0.23695033154354 0.05640361318484 0.55861478833680 v -0.21629590014574 0.05640361318484 0.55619447380179 v -0.19702312733742 0.05640361318484 0.52893856051672 v -0.17641432403605 0.05640361318484 0.48838976450056 v -0.15175181865480 0.05640361318484 0.44609086962469 v -0.12885726568490 0.05640361318484 0.40369671442018 v -0.11355232036948 0.05640361318484 0.36286223938463 v -0.09739498511590 0.05640361318484 0.32425000526786 v -0.07194324558775 0.05640361318484 0.28852260693386 v -0.04248612079274 0.05640361318484 0.24602633316673 v -0.01431263148692 0.05640361318484 0.18710742175161 v 0.01067685257304 0.05640361318484 0.15420826063910 v 0.03058196028790 0.05640361318484 0.18977120377919 v 0.04544043111681 0.05640361318484 0.24886574155731 v 0.05529000755028 0.05640361318484 0.28656131339524 v 0.07298721228214 0.05640361318484 0.30392778005720 v 0.11138856538066 0.05640361318484 0.30203507026944 # 292 vertices vt 0.56509065628052 -0.81152510643005 0.00000000000000 vt 0.59009295701981 -0.78740584850311 0.00000000000000 vt 0.58960372209549 -0.76500785350800 0.00000000000000 vt 0.90531194210052 -0.74878060817719 0.00000000000000 vt 0.93518441915512 -0.75294911861420 0.00000000000000 vt 0.94511520862579 -0.72258603572845 0.00000000000000 vt 0.31276014447212 -0.66935694217682 0.00000000000000 vt 0.29860022664070 -0.70226025581360 0.00000000000000 vt 0.39435943961143 -0.74488103389740 0.00000000000000 vt 0.57087159156799 -0.73520195484161 0.00000000000000 vt 0.48332142829895 -0.78736615180969 0.00000000000000 vt 0.86585867404938 -0.73726868629456 0.00000000000000 vt 0.91855299472809 -0.70829653739929 0.00000000000000 vt 0.54114532470703 -0.68885970115662 0.00000000000000 vt 0.45719981193542 -0.75626301765442 0.00000000000000 vt 0.53003823757172 -0.65375089645386 0.00000000000000 vt 0.83927667140961 -0.67679941654205 0.00000000000000 vt 0.82274514436722 -0.72734379768372 0.00000000000000 vt 0.56716364622116 -0.65764546394348 0.00000000000000 vt 0.62523347139359 -0.56721925735474 0.00000000000000 vt 0.74430227279663 -0.70101416110992 0.00000000000000 vt 0.78066259622574 -0.71619570255280 0.00000000000000 vt 0.79966193437576 -0.65810608863831 0.00000000000000 vt 0.35804006457329 -0.41128742694855 0.00000000000000 vt 0.60815954208374 -0.53656983375549 0.00000000000000 vt 0.70887809991837 -0.68776869773865 0.00000000000000 vt 0.76412415504456 -0.64040935039520 0.00000000000000 vt 0.31708452105522 -0.44059002399445 0.00000000000000 vt 0.42241942882538 -0.73933553695679 0.00000000000000 vt 0.31196892261505 -0.65044653415680 0.00000000000000 vt 0.69351863861084 -0.60931122303009 0.00000000000000 vt 0.66960418224335 -0.68242895603180 0.00000000000000 vt 0.62339460849762 -0.67553961277008 0.00000000000000 vt 0.65789425373077 -0.59183824062347 0.00000000000000 vt 0.37936788797379 -0.76090586185455 0.00000000000000 vt 0.27702710032463 -0.74103474617004 0.00000000000000 vt 0.94955557584763 -0.74084389209747 0.00000000000000 vt 0.31460902094841 -0.81914186477661 0.00000000000000 vt 0.25512927770615 -0.77816760540009 0.00000000000000 vt 0.61929565668106 -0.50100588798523 0.00000000000000 vt 0.54526948928833 -0.39776390790939 0.00000000000000 vt 0.64232176542282 -0.47496366500854 0.00000000000000 vt 0.56856691837311 -0.39240723848343 0.00000000000000 vt 0.91726356744766 -0.38884919881821 0.00000000000000 vt 0.92621064186096 -0.40586346387863 0.00000000000000 vt 0.94211983680725 -0.39492946863174 0.00000000000000 vt 0.23999537527561 -0.80614626407623 0.00000000000000 vt 0.29958060383797 -0.83498859405518 0.00000000000000 vt 0.68735671043396 -0.40141481161118 0.00000000000000 vt 0.66091769933701 -0.47287970781326 0.00000000000000 vt 0.90225237607956 -0.39692622423172 0.00000000000000 vt 0.91240793466568 -0.41515725851059 0.00000000000000 vt 0.58874547481537 -0.38363707065582 0.00000000000000 vt 0.72357970476151 -0.45469778776169 0.00000000000000 vt 0.67794829607010 -0.49646377563477 0.00000000000000 vt 0.70119422674179 -0.51496124267578 0.00000000000000 vt 0.22897557914257 -0.83246588706970 0.00000000000000 vt 0.28577327728271 -0.85995900630951 0.00000000000000 vt 0.90164363384247 -0.42043685913086 0.00000000000000 vt 0.70149928331375 -0.43156296014786 0.00000000000000 vt 0.67336577177048 -0.48372328281403 0.00000000000000 vt 0.68408465385437 -0.50793266296387 0.00000000000000 vt 0.72343027591705 -0.51812708377838 0.00000000000000 vt 0.74494612216949 -0.51800763607025 0.00000000000000 vt 0.74402111768723 -0.46184366941452 0.00000000000000 vt 0.89045006036758 -0.42564398050308 0.00000000000000 vt 0.89036566019058 -0.40052384138107 0.00000000000000 vt 0.76543283462524 -0.52294921875000 0.00000000000000 vt 0.87535947561264 -0.43472015857697 0.00000000000000 vt 0.87796592712402 -0.40307557582855 0.00000000000000 vt 0.78458166122437 -0.54129779338837 0.00000000000000 vt 0.81188714504242 -0.50484669208527 0.00000000000000 vt 0.82749015092850 -0.41562056541443 0.00000000000000 vt 0.83281779289246 -0.45556002855301 0.00000000000000 vt 0.85620456933975 -0.44543552398682 0.00000000000000 vt 0.86141586303711 -0.40801495313644 0.00000000000000 vt 0.80826544761658 -0.48674356937408 0.00000000000000 vt 0.75934678316116 -0.45517015457153 0.00000000000000 vt 0.81392836570740 -0.46827048063278 0.00000000000000 vt 0.81055569648743 -0.41617560386658 0.00000000000000 vt 0.79035347700119 -0.41489595174789 0.00000000000000 vt 0.82085138559341 -0.51644742488861 0.00000000000000 vt 0.80718684196472 -0.56132769584656 0.00000000000000 vt 0.83452415466309 -0.52646744251251 0.00000000000000 vt 0.83804273605347 -0.57131278514862 0.00000000000000 vt 0.87042796611786 -0.57438933849335 0.00000000000000 vt 0.87329167127609 -0.55117928981781 0.00000000000000 vt 0.91598075628281 -0.56980335712433 0.00000000000000 vt 0.92186504602432 -0.56329536437988 0.00000000000000 vt 0.91541802883148 -0.55735540390015 0.00000000000000 vt 0.89762121438980 -0.57369375228882 0.00000000000000 vt 0.89678376913071 -0.55516910552979 0.00000000000000 vt 0.93431156873703 -0.37907004356384 0.00000000000000 vt 0.95920383930206 -0.38472920656204 0.00000000000000 vt 0.77348303794861 -0.19764834642410 0.00000000000000 vt 0.78944236040115 -0.18458580970764 0.00000000000000 vt 0.77329629659653 -0.16294074058533 0.00000000000000 vt 0.21942020952702 -0.86462199687958 0.00000000000000 vt 0.26647987961769 -0.88740134239197 0.00000000000000 vt 0.21091064810753 -0.89598560333252 0.00000000000000 vt 0.95230865478516 -0.37036615610123 0.00000000000000 vt 0.97169548273087 -0.37576711177826 0.00000000000000 vt 0.75855153799057 -0.21077761054039 0.00000000000000 vt 0.75613719224930 -0.17449632287025 0.00000000000000 vt 0.97382777929306 -0.36854779720306 0.00000000000000 vt 0.96692425012589 -0.36582839488983 0.00000000000000 vt 0.74608081579208 -0.22293949127197 0.00000000000000 vt 0.73923271894455 -0.18503192067146 0.00000000000000 vt 0.77336710691452 -0.41928511857986 0.00000000000000 vt 0.76608020067215 -0.43684691190720 0.00000000000000 vt 0.73614275455475 -0.23281896114349 0.00000000000000 vt 0.72404271364212 -0.19343119859695 0.00000000000000 vt 0.69072902202606 -0.37322902679443 0.00000000000000 vt 0.59729540348053 -0.37003642320633 0.00000000000000 vt 0.70367842912674 -0.20524585247040 0.00000000000000 vt 0.72880905866623 -0.23910108208656 0.00000000000000 vt 0.69119691848755 -0.34954547882080 0.00000000000000 vt 0.66834092140198 -0.33290410041809 0.00000000000000 vt 0.69606006145477 -0.21160659193993 0.00000000000000 vt 0.72049319744110 -0.24458479881287 0.00000000000000 vt 0.58570712804794 -0.35018855333328 0.00000000000000 vt 0.64549571275711 -0.31998223066330 0.00000000000000 vt 0.68526804447174 -0.22172185778618 0.00000000000000 vt 0.70760887861252 -0.25206899642944 0.00000000000000 vt 0.64599597454071 -0.30745714902878 0.00000000000000 vt 0.60784083604813 -0.29146391153336 0.00000000000000 vt 0.67146116495132 -0.23478457331657 0.00000000000000 vt 0.69194853305817 -0.26275444030762 0.00000000000000 vt 0.62351757287979 -0.27798324823380 0.00000000000000 vt 0.65940964221954 -0.29389005899429 0.00000000000000 vt 0.63793218135834 -0.26512318849564 0.00000000000000 vt 0.67530465126038 -0.27784198522568 0.00000000000000 vt 0.65479797124863 -0.24998766183853 0.00000000000000 vt 0.22222928702831 -0.54956734180450 0.00000000000000 vt 0.80499631166458 -0.17262417078018 0.00000000000000 vt 0.78925025463104 -0.15148141980171 0.00000000000000 vt 0.81454157829285 -0.16142290830612 0.00000000000000 vt 0.80273205041885 -0.14556580781937 0.00000000000000 vt 0.26975423097610 -0.06945975124836 0.00000000000000 vt 0.26335111260414 -0.07571804523468 0.00000000000000 vt 0.26408633589745 -0.08214962482452 0.00000000000000 vt 0.81247472763062 -0.15064153075218 0.00000000000000 vt 0.28403186798096 -0.06973929703236 0.00000000000000 vt 0.27830979228020 -0.06644383072853 0.00000000000000 vt 0.58718901872635 -0.30846130847931 0.00000000000000 vt 0.57524883747101 -0.32827097177505 0.00000000000000 vt 0.27219355106354 -0.09732538461685 0.00000000000000 vt 0.29482179880142 -0.08330936729908 0.00000000000000 vt 0.52619153261185 -0.38691616058350 0.00000000000000 vt 0.51867151260376 -0.34707319736481 0.00000000000000 vt 0.39029085636139 -0.36821210384369 0.00000000000000 vt 0.51451694965363 -0.30187678337097 0.00000000000000 vt 0.40961194038391 -0.31177872419357 0.00000000000000 vt 0.24715922772884 -0.91353178024292 0.00000000000000 vt 0.20302835106850 -0.91992795467377 0.00000000000000 vt 0.40466326475143 -0.33491885662079 0.00000000000000 vt 0.50553536415100 -0.27496856451035 0.00000000000000 vt 0.41359153389931 -0.29916250705719 0.00000000000000 vt 0.28790637850761 -0.12981614470482 0.00000000000000 vt 0.31858116388321 -0.11111721396446 0.00000000000000 vt 0.30965271592140 -0.16312009096146 0.00000000000000 vt 0.34131243824959 -0.13664379715919 0.00000000000000 vt 0.41314262151718 -0.28230786323547 0.00000000000000 vt 0.33586025238037 -0.18073531985283 0.00000000000000 vt 0.36071723699570 -0.18938732147217 0.00000000000000 vt 0.36332488059998 -0.14599171280861 0.00000000000000 vt 0.23327013850212 -0.93456661701202 0.00000000000000 vt 0.19474586844444 -0.94048559665680 0.00000000000000 vt 0.34901830554008 -0.14337018132210 0.00000000000000 vt 0.49083572626114 -0.25168985128403 0.00000000000000 vt 0.46952688694000 -0.21738192439079 0.00000000000000 vt 0.40480589866638 -0.24645251035690 0.00000000000000 vt 0.39763396978378 -0.14221331477165 0.00000000000000 vt 0.37895885109901 -0.14769476652145 0.00000000000000 vt 0.37841182947159 -0.19580152630806 0.00000000000000 vt 0.45273652672768 -0.18429195880890 0.00000000000000 vt 0.39206701517105 -0.21111193299294 0.00000000000000 vt 0.51284867525101 -0.00226900354028 0.00000000000000 vt 0.50213211774826 -0.00000000000000 0.00000000000000 vt 0.49463811516762 -0.00794354267418 0.00000000000000 vt 0.45159214735031 -0.16466718912125 0.00000000000000 vt 0.41684129834175 -0.13131079077721 0.00000000000000 vt 0.46313729882240 -0.15177738666534 0.00000000000000 vt 0.49643689393997 -0.02420808002353 0.00000000000000 vt 0.52071756124496 -0.01664206013083 0.00000000000000 vt 0.52746534347534 -0.03927292674780 0.00000000000000 vt 0.48441550135612 -0.13889205455780 0.00000000000000 vt 0.45008635520935 -0.10478687286377 0.00000000000000 vt 0.53481847047806 -0.06631550192833 0.00000000000000 vt 0.50075405836105 -0.04674737900496 0.00000000000000 vt 0.50801330804825 -0.12542408704758 0.00000000000000 vt 0.48986327648163 -0.10269097983837 0.00000000000000 vt 0.53657108545303 -0.09205740690231 0.00000000000000 vt 0.50081545114517 -0.07351520657539 0.00000000000000 vt 0.47936615347862 -0.10258337855339 0.00000000000000 vt 0.46564581990242 -0.10167324542999 0.00000000000000 vt 0.49704408645630 -0.09525018930435 0.00000000000000 vt 0.52651727199554 -0.11078640818596 0.00000000000000 vt 0.35269153118134 -0.14268869161606 0.00000000000000 vt 0.18921807408333 -0.50487291812897 0.00000000000000 vt 0.21115991473198 -0.52864837646484 0.00000000000000 vt 0.27659785747528 -0.43256503343582 0.00000000000000 vt 0.18503575026989 -0.96169447898865 0.00000000000000 vt 0.21167291700840 -0.97458827495575 0.00000000000000 vt 0.54877799749374 -0.91113877296448 0.00000000000000 vt 0.51460713148117 -0.93316447734833 0.00000000000000 vt 0.53460383415222 -0.95466339588165 0.00000000000000 vt 0.16104736924171 -0.48245733976364 0.00000000000000 vt 0.23728826642036 -0.41033786535263 0.00000000000000 vt 0.53708213567734 -0.88952279090881 0.00000000000000 vt 0.49651327729225 -0.91217267513275 0.00000000000000 vt 0.13117377460003 -0.46165931224823 0.00000000000000 vt 0.19986389577389 -0.39703387022018 0.00000000000000 vt 0.48447722196579 -0.88881254196167 0.00000000000000 vt 0.17332282662392 -0.38572573661804 0.00000000000000 vt 0.09065736830235 -0.39747172594070 0.00000000000000 vt 0.10412329435349 -0.44273620843887 0.00000000000000 vt 0.48020562529564 -0.86042404174805 0.00000000000000 vt 0.53260153532028 -0.87179195880890 0.00000000000000 vt 0.16666318476200 -0.36948615312576 0.00000000000000 vt 0.10245556384325 -0.37163364887238 0.00000000000000 vt 0.52953559160233 -0.84625422954559 0.00000000000000 vt 0.48540520668030 -0.82434749603271 0.00000000000000 vt 0.16944034397602 -0.35037857294083 0.00000000000000 vt 0.11350409686565 -0.34867715835571 0.00000000000000 vt 0.53732788562775 -0.83269715309143 0.00000000000000 vt 0.17120972275734 -0.33046638965607 0.00000000000000 vt 0.11866450309753 -0.33043909072876 0.00000000000000 vt 0.38120642304420 -0.80802786350250 0.00000000000000 vt 0.39021295309067 -0.79044568538666 0.00000000000000 vt 0.38379293680191 -0.77541625499725 0.00000000000000 vt 0.16874919831753 -0.31347084045410 0.00000000000000 vt 0.15883676707745 -0.30311328172684 0.00000000000000 vt 0.55234849452972 -0.97954499721527 0.00000000000000 vt 0.57601571083069 -0.96428966522217 0.00000000000000 vt 0.22636914253235 -0.69108712673187 0.00000000000000 vt 0.18361742794514 -0.60698568820953 0.00000000000000 vt 0.15845535695553 -0.63835704326630 0.00000000000000 vt 0.11279834806919 -0.31875616312027 0.00000000000000 vt 0.14668984711170 -0.29642510414124 0.00000000000000 vt 0.25584641098976 -0.67014074325562 0.00000000000000 vt 0.20313899219036 -0.57946109771729 0.00000000000000 vt 0.13752593100071 -0.29043757915497 0.00000000000000 vt 0.12800784409046 -0.28495228290558 0.00000000000000 vt 0.56718534231186 -0.99794518947601 0.00000000000000 vt 0.58259356021881 -0.98801338672638 0.00000000000000 vt 0.21778261661530 -0.56341326236725 0.00000000000000 vt 0.10182480514050 -0.30984687805176 0.00000000000000 vt 0.11479850858450 -0.27977067232132 0.00000000000000 vt 0.08793735504150 -0.42242729663849 0.00000000000000 vt 0.09166307747364 -0.29992973804474 0.00000000000000 vt 0.09843736141920 -0.27238398790359 0.00000000000000 vt 0.03565083071589 -0.23923555016518 0.00000000000000 vt 0.03359920158982 -0.24620661139488 0.00000000000000 vt 0.03885761275887 -0.25306904315948 0.00000000000000 vt 0.05027095228434 -0.23901826143265 0.00000000000000 vt 0.04215918481350 -0.23566851019859 0.00000000000000 vt 0.08007917553186 -0.28841000795364 0.00000000000000 vt 0.07946395128965 -0.26028358936310 0.00000000000000 vt 0.57845854759216 -1.00000000000000 0.00000000000000 vt 0.06483913958073 -0.27469289302826 0.00000000000000 vt 0.06202593073249 -0.24773865938187 0.00000000000000 vt 0.04980970919132 -0.26187902688980 0.00000000000000 vt 0.28868865966797 -0.65365087985992 0.00000000000000 vt 0.19500856101513 -0.71123790740967 0.00000000000000 vt 0.12689012289047 -0.66594529151917 0.00000000000000 vt 0.33756572008133 -0.81907033920288 0.00000000000000 vt 0.36193633079529 -0.82021772861481 0.00000000000000 vt 0.09508404880762 -0.68877577781677 0.00000000000000 vt 0.11851247400045 -0.73592782020569 0.00000000000000 vt 0.06919944286346 -0.70587420463562 0.00000000000000 vt 0.08861683309078 -0.74553108215332 0.00000000000000 vt 0.04564516618848 -0.72076189517975 0.00000000000000 vt 0.06196573004127 -0.75467634201050 0.00000000000000 vt 0.18054743111134 -0.97945725917816 0.00000000000000 vt 0.18793037533760 -0.98967611789703 0.00000000000000 vt 0.20052531361580 -0.98912763595581 0.00000000000000 vt 0.02082998305559 -0.73696041107178 0.00000000000000 vt 0.03369546681643 -0.76388943195343 0.00000000000000 vt 0.00289968517609 -0.75201618671417 0.00000000000000 vt 0.00000000000000 -0.76347541809082 0.00000000000000 vt 0.01073165331036 -0.76840949058533 0.00000000000000 vt 0.87992233037949 -0.69426965713501 0.00000000000000 vt 0.72972315549850 -0.62403571605682 0.00000000000000 vt 0.84362185001373 -0.41298300027847 0.00000000000000 vt 0.85227137804031 -0.53982830047607 0.00000000000000 vt 0.71178525686264 -0.20005047321320 0.00000000000000 vt 0.22228421270847 -0.95381557941437 0.00000000000000 vt 0.43407180905342 -0.11675067245960 0.00000000000000 vt 0.56323909759521 -0.93620574474335 0.00000000000000 vt 0.53139853477478 -0.85751342773438 0.00000000000000 vt 0.15651637315750 -0.72534084320068 0.00000000000000 # 292 texture coordinates o Extrude.1 usemtl default f 292/1 291/2 290/3 f 278/4 277/5 275/6 f 51/7 52/8 21/9 f 292/1 289/10 20/11 f 279/12 278/4 274/13 f 20/11 289/10 288/14 f 19/15 288/14 287/16 f 272/17 280/18 279/12 f 287/16 286/19 266/20 f 282/21 281/22 271/23 f 281/22 280/18 272/17 f 106/24 287/16 265/25 f 283/26 282/21 270/27 f 287/16 106/24 105/28 f 22/29 287/16 50/30 f 268/31 284/32 283/26 f 266/20 286/19 285/33 f 267/34 285/33 284/32 f 24/35 52/8 46/36 f 52/8 24/35 21/9 f 277/5 276/37 275/6 f 30/38 46/36 45/39 f 265/25 264/40 168/41 f 264/40 263/42 169/43 f 224/44 232/45 231/46 f 45/39 48/47 29/48 f 209/49 263/42 262/50 f 223/51 233/52 232/45 f 263/42 209/49 170/53 f 211/54 260/55 258/56 f 48/47 47/57 32/58 f 234/59 233/52 223/51 f 210/60 262/50 261/61 f 261/61 260/55 211/54 f 260/55 259/62 258/56 f 257/63 256/64 212/65 f 235/66 234/59 222/67 f 256/64 255/68 212/65 f 236/69 235/66 221/70 f 255/68 254/71 241/72 f 218/73 238/74 237/75 f 237/75 236/69 220/76 f 255/68 240/77 213/78 f 239/79 217/80 216/81 f 242/82 254/71 253/83 f 217/80 239/79 238/74 f 254/71 242/82 241/72 f 213/78 240/77 239/79 f 243/84 253/83 252/85 f 252/85 251/86 245/87 f 249/88 248/89 247/90 f 251/86 250/91 246/92 f 246/92 250/91 249/88 f 225/93 231/46 230/94 f 194/95 193/96 187/97 f 32/58 47/57 42/98 f 31/99 42/98 41/100 f 226/101 230/94 229/102 f 195/103 194/95 186/104 f 229/102 228/105 227/106 f 196/107 195/103 185/108 f 216/81 215/109 214/110 f 197/111 196/107 184/112 f 209/49 208/113 171/114 f 182/115 198/116 197/111 f 208/113 207/117 206/118 f 171/114 208/113 206/118 f 181/119 199/120 198/116 f 172/121 171/114 205/122 f 180/123 200/124 199/120 f 205/122 204/125 175/126 f 179/127 201/128 200/124 f 176/129 204/125 203/130 f 177/131 203/130 202/132 f 178/133 202/132 201/128 f 204/125 176/129 175/126 f 77/134 50/30 105/28 f 193/96 192/135 188/136 f 192/135 191/137 189/138 f 121/139 122/140 123/141 f 191/137 190/142 189/138 f 137/143 138/144 121/139 f 174/145 173/146 172/121 f 124/147 140/148 137/143 f 165/149 166/150 120/151 f 166/150 167/152 118/153 f 35/154 41/100 44/155 f 166/150 119/156 120/151 f 167/152 161/157 117/158 f 125/159 139/160 140/148 f 126/161 142/162 139/160 f 116/163 117/158 161/157 f 127/164 128/165 143/166 f 36/167 44/155 43/168 f 126/161 127/164 141/169 f 162/170 163/171 115/172 f 129/173 130/174 113/175 f 128/165 113/175 130/174 f 163/171 164/176 114/177 f 159/178 158/179 157/180 f 164/176 152/181 129/173 f 132/182 152/181 151/183 f 156/184 160/185 159/178 f 152/181 132/182 129/173 f 145/186 160/185 156/184 f 151/183 150/187 134/188 f 146/189 145/186 155/190 f 150/187 149/191 135/192 f 147/193 146/189 154/194 f 150/187 136/195 133/196 f 153/197 149/191 148/198 f 149/191 153/197 135/192 f 143/166 144/199 141/169 f 79/200 78/201 108/202 f 108/202 78/201 77/134 f 43/168 38/203 34/204 f 6/205 14/206 13/207 f 80/208 79/200 107/209 f 5/210 15/211 14/206 f 73/212 80/208 110/213 f 16/214 15/211 5/210 f 109/215 76/216 74/217 f 18/218 16/214 4/219 f 109/215 112/220 86/221 f 2/222 17/223 18/218 f 112/220 111/224 85/225 f 1/226 20/11 17/223 f 111/224 98/227 88/228 f 25/229 26/230 23/231 f 98/227 97/232 100/233 f 13/207 12/234 8/235 f 88/228 98/227 100/233 f 55/236 66/237 65/238 f 87/239 88/228 99/240 f 56/241 67/242 66/237 f 87/239 102/243 101/244 f 12/234 11/245 9/246 f 50/30 77/134 68/247 f 82/248 101/244 104/249 f 76/216 75/250 74/217 f 81/251 104/249 103/252 f 95/253 96/254 93/255 f 93/255 89/256 90/257 f 84/258 103/252 91/259 f 11/245 10/260 9/246 f 83/261 91/259 92/262 f 94/263 92/262 89/256 f 49/264 68/247 67/242 f 54/265 65/238 72/266 f 23/231 27/267 28/268 f 72/266 71/269 60/270 f 24/35 30/38 27/267 f 71/269 70/271 59/272 f 70/271 69/273 58/274 f 37/275 40/276 39/277 f 69/273 61/278 57/279 f 57/279 61/278 62/280 f 62/280 63/281 64/282 f 38/203 37/275 39/277 f 292/1 290/3 289/10 f 278/4 275/6 274/13 f 51/7 21/9 22/29 f 292/1 20/11 1/226 f 279/12 274/13 273/283 f 20/11 288/14 19/15 f 19/15 287/16 22/29 f 272/17 279/12 273/283 f 287/16 266/20 265/25 f 282/21 271/23 270/27 f 281/22 272/17 271/23 f 106/24 265/25 165/149 f 283/26 270/27 269/284 f 287/16 105/28 50/30 f 22/29 50/30 51/7 f 268/31 283/26 269/284 f 266/20 285/33 267/34 f 267/34 284/32 268/31 f 24/35 46/36 30/38 f 30/38 45/39 29/48 f 265/25 168/41 165/149 f 264/40 169/43 168/41 f 224/44 231/46 225/93 f 209/49 262/50 210/60 f 223/51 232/45 224/44 f 263/42 170/53 169/43 f 211/54 258/56 257/63 f 48/47 32/58 29/48 f 234/59 223/51 222/67 f 210/60 261/61 211/54 f 257/63 212/65 211/54 f 235/66 222/67 221/70 f 236/69 221/70 220/76 f 255/68 241/72 240/77 f 218/73 237/75 219/285 f 237/75 220/76 219/285 f 255/68 213/78 212/65 f 239/79 216/81 214/110 f 242/82 253/83 243/84 f 217/80 238/74 218/73 f 213/78 239/79 214/110 f 243/84 252/85 244/286 f 252/85 245/87 244/286 f 251/86 246/92 245/87 f 246/92 249/88 247/90 f 225/93 230/94 226/101 f 194/95 187/97 186/104 f 32/58 42/98 31/99 f 31/99 41/100 35/154 f 226/101 229/102 227/106 f 195/103 186/104 185/108 f 196/107 185/108 184/112 f 197/111 184/112 183/287 f 209/49 171/114 170/53 f 182/115 197/111 183/287 f 171/114 206/118 205/122 f 181/119 198/116 182/115 f 172/121 205/122 174/145 f 180/123 199/120 181/119 f 205/122 175/126 174/145 f 179/127 200/124 180/123 f 176/129 203/130 177/131 f 177/131 202/132 178/133 f 178/133 201/128 179/127 f 77/134 105/28 108/202 f 193/96 188/136 187/97 f 192/135 189/138 188/136 f 137/143 121/139 123/141 f 124/147 137/143 123/141 f 165/149 120/151 106/24 f 166/150 118/153 119/156 f 35/154 44/155 36/167 f 167/152 117/158 118/153 f 125/159 140/148 124/147 f 126/161 139/160 125/159 f 116/163 161/157 162/170 f 127/164 143/166 141/169 f 36/167 43/168 33/288 f 126/161 141/169 142/162 f 162/170 115/172 116/163 f 129/173 113/175 114/177 f 128/165 130/174 143/166 f 163/171 114/177 115/172 f 164/176 129/173 114/177 f 132/182 151/183 131/289 f 156/184 159/178 157/180 f 145/186 156/184 155/190 f 151/183 134/188 131/289 f 146/189 155/190 154/194 f 150/187 135/192 136/195 f 147/193 154/194 153/197 f 150/187 133/196 134/188 f 153/197 148/198 147/193 f 79/200 108/202 107/209 f 43/168 34/204 33/288 f 6/205 13/207 7/290 f 80/208 107/209 110/213 f 5/210 14/206 6/205 f 73/212 110/213 109/215 f 16/214 5/210 4/219 f 109/215 74/217 73/212 f 18/218 4/219 3/291 f 109/215 86/221 76/216 f 2/222 18/218 3/291 f 112/220 85/225 86/221 f 1/226 17/223 2/222 f 111/224 88/228 85/225 f 13/207 8/235 7/290 f 88/228 100/233 99/240 f 55/236 65/238 54/265 f 87/239 99/240 102/243 f 56/241 66/237 55/236 f 87/239 101/244 82/248 f 12/234 9/246 8/235 f 50/30 68/247 49/264 f 82/248 104/249 81/251 f 81/251 103/252 84/258 f 93/255 90/257 95/253 f 84/258 91/259 83/261 f 83/261 92/262 94/263 f 94/263 89/256 93/255 f 49/264 67/242 56/241 f 54/265 72/266 53/292 f 23/231 28/268 25/229 f 72/266 60/270 53/292 f 24/35 27/267 23/231 f 71/269 59/272 60/270 f 70/271 58/274 59/272 f 69/273 57/279 58/274 f 57/279 62/280 64/282 f 38/203 39/277 34/204 ================================================ FILE: static/models/stump.obj ================================================ # Blender v2.76 (sub 0) OBJ File: 'stump.blend' # www.blender.org mtllib stump-test.mtl o Mesh v -0.107668 0.351923 -0.107757 v 0.000101 0.347774 -0.144886 v 0.000257 0.214742 -0.144888 v -0.109719 0.223690 -0.106736 v 0.107762 0.351923 -0.110072 v 0.107801 0.223690 -0.109998 v 0.136634 0.137433 -0.141185 v 0.055548 0.154955 -0.160610 v -0.000675 0.130719 -0.165272 v -0.056783 0.161377 -0.158195 v -0.141379 0.142411 -0.136948 v 0.109789 0.351923 0.104730 v 0.002879 0.347774 0.141702 v 0.003374 0.214742 0.142534 v 0.109914 0.223690 0.105013 v -0.105499 0.351923 0.107392 v -0.107042 0.223690 0.109664 v -0.131795 0.143986 0.136709 v -0.049364 0.141648 0.145143 v 0.005628 0.125335 0.155586 v 0.057414 0.157698 0.152116 v 0.136898 0.141991 0.135082 v 0.144538 0.347774 -0.002964 v 0.144541 0.214742 -0.002667 v 0.149447 0.160467 0.053329 v 0.154151 0.127795 -0.000430 v 0.152122 0.150888 -0.056671 v -0.144561 0.347774 0.000964 v -0.152922 0.214742 0.005050 v -0.175700 0.157093 -0.045602 v -0.200043 0.131801 0.021821 v -0.171581 0.161085 0.069900 v 0.136215 0.079403 -0.312582 v 0.137637 0.043276 -0.317976 v 0.099349 0.044578 -0.324281 v 0.097306 0.080162 -0.319005 v 0.136830 0.007921 -0.308153 v 0.097347 0.009692 -0.314579 v 0.068812 0.019463 -0.316710 v 0.060783 0.044195 -0.326107 v 0.068783 0.069810 -0.319864 v 0.474721 0.036402 0.170602 v 0.479890 0.028470 0.175671 v 0.491367 0.029061 0.169278 v 0.483817 0.038952 0.165113 v 0.474259 0.018476 0.172450 v 0.483246 0.016808 0.167397 v 0.480155 0.017520 0.157206 v 0.487174 0.027290 0.156839 v 0.480617 0.035446 0.155357 v -0.125825 0.067754 0.155076 v -0.066405 0.065528 0.148309 v -0.134483 0.004155 0.147637 v -0.066134 0.009400 0.150591 v 0.005160 0.001275 0.156483 v -0.011483 0.063811 0.159083 v -0.361263 0.055539 -0.069580 v -0.370353 0.038320 -0.073999 v -0.366382 0.034187 -0.042829 v -0.358653 0.057358 -0.046580 v -0.361136 0.018785 -0.067782 v -0.358383 0.010862 -0.044225 v -0.351464 0.005698 -0.007857 v -0.359411 0.029772 -0.014504 v -0.351940 0.053232 -0.010166 v -0.078790 1.285750 0.080271 v 0.002381 1.284592 0.105636 v 0.001382 1.283103 -0.001731 v -0.105985 1.284592 -0.000731 v 0.081653 1.285750 0.077060 v 0.108748 1.284592 -0.002730 v 0.080172 1.285750 -0.082002 v 0.000382 1.284592 -0.109097 v -0.078890 1.285750 -0.080521 v 0.114062 0.000808 0.116066 v 0.074710 0.002270 0.129907 v 0.074205 0.003456 0.075607 v 0.127511 0.002270 0.076471 v 0.003474 0.002118 0.130420 v 0.002878 0.002849 0.075365 v 0.002204 0.001938 0.002923 v 0.073535 0.002849 0.003619 v 0.126847 0.002118 0.005163 v -0.106984 0.688060 -0.108098 v 0.000049 0.688060 -0.144886 v 0.000049 0.522066 -0.144886 v -0.106984 0.522066 -0.108098 v 0.107749 0.688060 -0.110097 v 0.107749 0.522066 -0.110097 v 0.144537 0.688060 -0.003063 v 0.144537 0.522066 -0.003063 v 0.109747 0.688060 0.104636 v 0.109747 0.522066 0.104636 v 0.002714 0.688060 0.141425 v 0.002714 0.522066 0.141425 v -0.104985 0.688060 0.106635 v -0.104985 0.522066 0.106635 v -0.141774 0.688060 -0.000398 v -0.141774 0.522066 -0.000398 v -0.106984 0.994652 -0.108098 v 0.000049 0.994652 -0.144886 v 0.000049 0.843228 -0.144886 v -0.106984 0.843228 -0.108098 v 0.107749 0.994652 -0.110097 v 0.107749 0.843228 -0.110097 v 0.144537 0.994652 -0.003063 v 0.144537 0.843228 -0.003063 v 0.109747 0.994652 0.104636 v 0.109747 0.843228 0.104636 v 0.002714 0.994652 0.141425 v 0.002714 0.843228 0.141425 v -0.104985 0.994652 0.106635 v -0.104985 0.843228 0.106635 v -0.141774 0.994652 -0.000398 v -0.141774 0.843228 -0.000398 v -0.106984 1.266596 -0.108098 v 0.000049 1.266596 -0.144886 v 0.000049 1.141063 -0.144886 v -0.106984 1.141063 -0.108098 v 0.107749 1.266596 -0.110097 v 0.107749 1.141063 -0.110097 v 0.144537 1.266596 -0.003063 v 0.144537 1.141063 -0.003063 v 0.109747 1.266596 0.104636 v 0.109747 1.141063 0.104636 v 0.002714 1.266596 0.141425 v 0.002714 1.141063 0.141425 v -0.104985 1.266596 0.106635 v -0.104985 1.141063 0.106635 v -0.141774 1.266596 -0.000398 v -0.141774 1.141063 -0.000398 v 0.000049 1.289059 -0.144886 v -0.106984 1.289059 -0.108098 v 0.107749 1.289059 -0.110097 v 0.144537 1.289059 -0.003063 v 0.109747 1.289059 0.104636 v 0.002714 1.289059 0.141425 v -0.104985 1.289059 0.106635 v -0.141774 1.289059 -0.000398 v 0.128008 -0.003717 0.129864 v 0.074879 -0.003717 0.148007 v 0.140650 -0.001646 0.143780 v 0.075340 -0.010308 0.158781 v 0.003896 -0.003717 0.148972 v 0.111989 0.000808 -0.106620 v 0.125676 -0.003717 -0.120658 v 0.143947 -0.003717 -0.066396 v 0.126179 0.002270 -0.066684 v 0.138672 -0.002905 -0.136859 v 0.152022 -0.011087 -0.067433 v 0.152968 -0.002516 0.006061 v 0.144618 -0.003717 0.005677 v -0.112223 0.000808 -0.104020 v -0.128278 -0.003717 -0.117140 v -0.070942 -0.003717 -0.136971 v -0.070777 0.002270 -0.119282 v -0.143284 -0.000335 -0.132857 v -0.074540 -0.004177 -0.151683 v -0.000086 0.000073 -0.152973 v 0.000895 -0.003717 -0.137640 v 0.001058 0.002118 -0.120178 v -0.109257 0.000808 0.119471 v -0.123936 -0.003717 0.135193 v -0.153422 -0.003717 0.085375 v -0.126108 0.002270 0.080291 v -0.173527 -0.005531 0.099825 v -0.196234 -0.000869 0.028207 v -0.161954 -0.003717 0.015759 v -0.128740 0.002118 0.009349 v -0.254519 0.008260 -0.557969 v -0.262327 0.023454 -0.563748 v -0.250949 0.024216 -0.581260 v -0.246265 0.003742 -0.571309 v -0.254519 0.037463 -0.557969 v -0.246265 0.043167 -0.571309 v -0.230724 0.037463 -0.569170 v -0.230204 0.023454 -0.578869 v -0.230724 0.008260 -0.569170 v 0.280622 0.152459 0.125233 v 0.294723 0.162988 0.093084 v 0.239857 0.161826 0.086945 v 0.227248 0.148207 0.124915 v 0.303917 0.149706 0.061388 v 0.253803 0.148053 0.050894 v 0.206655 0.130570 0.042850 v 0.190889 0.144880 0.083197 v 0.181805 0.128381 0.126670 v -0.068950 0.003456 0.076940 v -0.068110 0.002270 0.131541 v -0.069621 0.002849 0.004952 v -0.066937 -0.003717 0.150547 v 0.072213 -0.003717 -0.138304 v 0.073587 -0.008446 -0.153693 v 0.072378 0.002270 -0.120615 v 0.516658 0.000600 -0.159741 v 0.520929 0.010119 -0.164918 v 0.530976 0.009807 -0.155152 v 0.524230 -0.003060 -0.152380 v 0.517259 0.020124 -0.160888 v 0.525041 0.023298 -0.153928 v 0.522750 0.020124 -0.143460 v 0.528342 0.010119 -0.141391 v 0.522150 0.000600 -0.142314 v -0.288215 0.060304 0.319034 v -0.295491 0.077194 0.332379 v -0.314163 0.077280 0.321458 v -0.307520 0.060392 0.307851 v -0.295996 0.099842 0.333004 v -0.315301 0.099930 0.321821 v -0.339377 0.099656 0.314246 v -0.337624 0.077003 0.314240 v -0.331000 0.060118 0.300783 v 0.072872 0.003456 -0.067548 v 0.001546 0.002849 -0.067791 v 0.145280 -0.003717 0.076759 v 0.154751 -0.006476 0.079656 v -0.156094 -0.003717 -0.058988 v -0.175477 -0.009631 -0.056602 v -0.127776 0.002270 -0.063166 v -0.070283 0.003456 -0.066215 v -0.066807 0.047096 -0.262908 v -0.055062 0.089334 -0.267738 v -0.026656 0.077830 -0.208627 v -0.041318 0.027871 -0.204518 v -0.066814 0.131572 -0.263588 v -0.041343 0.127790 -0.207238 v -0.001498 0.066185 -0.175639 v -0.102059 0.145651 -0.250229 v -0.085370 0.144444 -0.199444 v -0.137302 0.131572 -0.236644 v -0.129389 0.127790 -0.190743 v -0.149047 0.089334 -0.231814 v -0.144050 0.077830 -0.186633 v -0.137296 0.047096 -0.235964 v -0.129363 0.027871 -0.188023 v -0.155814 0.066811 -0.149458 v -0.102050 0.033016 -0.249323 v -0.085336 0.011217 -0.195817 v -0.118782 0.065282 -0.342216 v -0.110353 0.096315 -0.346183 v -0.084725 0.099239 -0.310356 v -0.094395 0.063637 -0.305804 v -0.118782 0.127347 -0.342216 v -0.094395 0.134841 -0.305804 v -0.144067 0.137692 -0.330313 v -0.123403 0.146709 -0.292149 v -0.169352 0.127347 -0.318411 v -0.152412 0.134841 -0.278494 v -0.177781 0.096315 -0.314443 v -0.162082 0.099239 -0.273942 v -0.169352 0.065282 -0.318411 v -0.152412 0.063637 -0.278494 v -0.144067 0.054938 -0.330313 v -0.123403 0.051769 -0.292149 v -0.149784 0.010256 -0.422286 v -0.142102 0.038540 -0.425902 v -0.126653 0.069328 -0.384926 v -0.134668 0.039818 -0.381153 v -0.149784 0.066825 -0.422286 v -0.134668 0.098837 -0.381153 v -0.172830 0.076254 -0.411437 v -0.158712 0.108674 -0.369835 v -0.195877 0.066825 -0.400589 v -0.182757 0.098837 -0.358517 v -0.203559 0.038540 -0.396973 v -0.190771 0.069328 -0.354744 v -0.195877 0.010256 -0.400589 v -0.182757 0.039818 -0.358517 v -0.172830 0.000827 -0.411437 v -0.158712 0.029982 -0.369835 v -0.242105 0.094666 -0.023947 v -0.242193 0.050083 -0.007523 v -0.220098 0.058356 0.007230 v -0.217225 0.110070 -0.012394 v -0.241764 0.005847 -0.019817 v -0.216212 0.006642 -0.007789 v -0.214323 0.066014 0.030928 v -0.241011 -0.008689 -0.062187 v -0.206163 -0.010596 -0.058928 v -0.241811 -0.000410 -0.114820 v -0.196322 0.006642 -0.111631 v -0.231676 0.048580 -0.130108 v -0.193062 0.058356 -0.131346 v -0.241823 0.097566 -0.119364 v -0.196561 0.110070 -0.116417 v -0.241305 0.110240 -0.067727 v -0.206997 0.127308 -0.065188 v -0.291674 0.060946 -0.024358 v -0.282187 0.031388 -0.013695 v -0.264670 0.039609 -0.017152 v -0.266889 0.075820 -0.030828 v -0.291108 0.001027 -0.021261 v -0.266616 0.004783 -0.027361 v -0.303534 -0.002766 -0.059310 v -0.272880 -0.005987 -0.062602 v -0.315211 0.004218 -0.100658 v -0.278860 -0.006829 -0.106410 v 0.476358 0.000409 -0.150643 v 0.479955 -0.004121 -0.138185 v 0.455320 0.000566 -0.130335 v 0.451645 0.005505 -0.144103 v 0.484022 0.000409 -0.126320 v 0.460062 0.005505 -0.117392 v 0.433810 0.012271 -0.106058 v 0.428031 0.006363 -0.121542 v 0.423722 0.012271 -0.138075 v -0.315241 0.070788 -0.103779 v -0.303898 0.074466 -0.063127 v -0.273116 0.090742 -0.067239 v -0.278823 0.088663 -0.110845 v -0.255138 -0.010707 -0.163060 v -0.248264 0.021076 -0.161888 v -0.247540 0.033595 -0.143555 v -0.255785 -0.003675 -0.142102 v -0.254393 0.052814 -0.165728 v -0.255383 0.069566 -0.145335 v -0.273837 0.058166 -0.173472 v -0.279480 0.073868 -0.146103 v -0.299744 0.045252 -0.185703 v -0.303695 0.056735 -0.145924 v -0.310020 0.020272 -0.180359 v -0.311891 0.028402 -0.144865 v -0.300387 -0.005866 -0.183581 v -0.303999 0.001367 -0.143480 v -0.325485 0.036520 -0.110443 v -0.274708 -0.016195 -0.170349 v -0.279951 -0.011871 -0.142318 v -0.242540 0.000186 -0.189697 v -0.236861 0.020780 -0.189015 v -0.241456 0.018699 -0.175308 v -0.246830 -0.010313 -0.178241 v -0.241897 0.040486 -0.191428 v -0.245936 0.047983 -0.180706 v -0.248052 0.045756 -0.205225 v -0.260642 0.053364 -0.193413 v -0.263641 0.042239 -0.224454 v -0.279652 0.048506 -0.209564 v -0.331474 0.039444 -0.287592 v -0.336803 0.021707 -0.290001 v -0.348680 0.018074 -0.273269 v -0.344088 0.035447 -0.269793 v -0.332053 0.004231 -0.286076 v -0.344620 0.000923 -0.268330 v -0.368051 0.000364 -0.254963 v -0.367934 0.020149 -0.263763 v -0.367173 0.037456 -0.255807 v -0.264335 0.000098 -0.222619 v -0.248866 -0.003685 -0.203073 v -0.261687 -0.014880 -0.190527 v -0.280540 -0.009413 -0.207114 v 0.510557 0.011056 -0.131476 v 0.508916 -0.002123 -0.134624 v 0.509727 0.024236 -0.136172 v 0.485431 0.027588 -0.128100 v 0.486004 0.013998 -0.123156 v 0.506156 0.028629 -0.148193 v 0.502314 0.024236 -0.159699 v 0.477767 0.027588 -0.152423 v 0.481834 0.032117 -0.140559 v 0.500673 0.011056 -0.162846 v 0.501503 -0.002123 -0.158151 v 0.475785 0.013998 -0.155587 v 0.505074 -0.006516 -0.146129 v -0.335081 0.040700 -0.219954 v -0.336910 0.017230 -0.209640 v -0.319828 0.017293 -0.202336 v -0.313938 0.038932 -0.208652 v -0.335736 -0.005906 -0.218047 v -0.314541 -0.003305 -0.206914 v -0.314673 -0.008451 -0.238786 v -0.297900 -0.008322 -0.222950 v -0.295958 -0.001333 -0.262085 v -0.281046 0.000510 -0.239572 v -0.286745 0.023372 -0.266241 v -0.275120 0.021459 -0.245908 v -0.295178 0.045570 -0.264123 v -0.280372 0.041365 -0.241352 v -0.258618 0.021709 -0.228602 v -0.313829 0.046366 -0.241108 v -0.297049 0.047073 -0.225295 v -0.402088 0.036711 -0.225626 v -0.407486 0.020346 -0.221314 v -0.393117 0.018600 -0.204003 v -0.387512 0.037234 -0.208879 v -0.403100 0.003844 -0.225341 v -0.388030 -0.000516 -0.207348 v -0.375552 0.003592 -0.199975 v -0.376616 0.018517 -0.193615 v -0.375148 0.032987 -0.201167 v -0.331765 -0.006644 -0.255374 v -0.351308 -0.007787 -0.236632 v -0.317065 -0.003438 -0.276229 v -0.368412 0.004127 -0.410883 v -0.373286 0.009242 -0.413751 v -0.373399 0.008838 -0.423493 v -0.368529 0.001905 -0.418346 v -0.368323 0.014597 -0.411436 v -0.368407 0.016191 -0.419101 v -0.360848 0.014901 -0.419178 v -0.363650 0.008854 -0.423697 v -0.360948 0.003221 -0.418561 v -0.330990 0.043684 -0.257507 v -0.316222 0.047894 -0.278439 v -0.350596 0.042545 -0.238697 v 0.220820 0.084723 0.033571 v 0.182445 0.072602 0.019589 v 0.172455 0.124347 0.031091 v 0.226702 0.038145 0.052232 v 0.187711 0.020263 0.039621 v 0.162104 0.060909 0.003208 v 0.217618 0.021647 0.095705 v 0.183166 0.002024 0.088344 v 0.201852 0.035956 0.136052 v 0.173536 0.018480 0.134224 v 0.187687 0.081804 0.145331 v 0.163545 0.070226 0.145726 v 0.158280 0.122565 0.125694 v 0.148959 0.064761 0.152688 v 0.162824 0.140803 0.076971 v -0.240068 0.055283 0.380217 v -0.230174 0.036266 0.376657 v -0.235914 0.065036 0.342035 v -0.245612 0.085434 0.350502 v -0.231668 0.014930 0.371744 v -0.236276 0.040719 0.338030 v -0.247190 0.065468 0.312815 v -0.247871 0.091330 0.315339 v -0.256461 0.111895 0.328387 v -0.247793 0.006862 0.368751 v -0.250306 0.029753 0.343305 v -0.266496 0.011178 0.368358 v -0.267201 0.032510 0.352408 v -0.271656 0.053262 0.345623 v -0.258000 0.052241 0.326829 v -0.275722 0.026993 0.371245 v -0.276159 0.049359 0.359886 v -0.273562 0.045126 0.375485 v -0.275055 0.070127 0.362900 v -0.279456 0.092320 0.358723 v -0.279511 0.070142 0.357435 v -0.258104 0.056396 0.379151 v -0.261766 0.084641 0.358615 v -0.269381 0.109231 0.345945 v -0.259674 0.023628 0.457260 v -0.254813 0.011977 0.457113 v -0.240522 0.020026 0.419634 v -0.248044 0.035349 0.420687 v -0.257098 -0.000229 0.455064 v -0.242627 0.003515 0.415945 v -0.267525 -0.003777 0.451960 v -0.256451 -0.001890 0.411451 v -0.278742 -0.000003 0.449531 v -0.271938 0.002475 0.408412 v -0.283399 0.009755 0.449503 v -0.279031 0.015271 0.409089 v -0.280910 0.020068 0.451378 v -0.276496 0.029256 0.412402 v -0.270688 0.025509 0.454656 v -0.263101 0.037187 0.417272 v -0.356860 0.017850 -0.201113 v -0.360297 0.038283 -0.206777 v -0.360859 -0.002689 -0.205115 v -0.372079 -0.007528 -0.219424 v -0.388593 -0.001838 -0.237800 v -0.563199 0.001328 -0.309745 v -0.567424 0.004799 -0.309362 v -0.570489 0.005196 -0.317018 v -0.565728 0.000474 -0.315798 v -0.562429 0.008881 -0.308646 v -0.564721 0.010339 -0.314363 v -0.559061 0.009812 -0.317402 v -0.563025 0.006014 -0.320798 v -0.559832 0.002259 -0.318501 v -0.371387 0.042805 -0.221466 v -0.387244 0.041984 -0.238181 v -0.294008 0.011410 -0.327827 v -0.287550 0.033029 -0.329266 v -0.286510 0.029939 -0.295715 v -0.292836 0.007748 -0.296093 v -0.293350 0.054678 -0.329586 v -0.292120 0.051994 -0.297932 v -0.311661 0.059666 -0.328108 v -0.309944 0.056845 -0.302034 v -0.330175 0.051366 -0.326091 v -0.327988 0.048107 -0.305572 v -0.336581 0.033150 -0.324790 v -0.334258 0.029395 -0.306094 v -0.330730 0.014903 -0.324608 v -0.328591 0.010820 -0.304022 v -0.312470 0.006512 -0.325947 v -0.310824 0.002490 -0.299775 v -0.327945 0.002254 -0.387876 v -0.325208 0.013566 -0.390361 v -0.303003 0.024509 -0.361829 v -0.307767 0.007684 -0.359535 v -0.327710 0.025027 -0.388999 v -0.307315 0.041452 -0.360997 v -0.335539 0.027853 -0.383359 v -0.320879 0.045487 -0.356476 v -0.343441 0.023684 -0.377375 v -0.334581 0.039151 -0.351506 v -0.346159 0.014163 -0.374979 v -0.339310 0.024981 -0.349327 v -0.343639 0.004493 -0.376428 v -0.334962 0.010694 -0.350274 v -0.335828 -0.000123 -0.381980 v -0.321434 0.004003 -0.354680 v -0.271617 0.041319 0.128646 v -0.274183 0.087136 0.105208 v -0.240805 0.074645 0.067388 v -0.236272 0.023066 0.089940 v -0.254750 0.132833 0.101089 v -0.226063 0.126170 0.070198 v -0.218941 0.147904 0.125474 v -0.195449 0.143274 0.104952 v -0.188754 0.132470 0.159045 v -0.168238 0.126010 0.146287 v -0.186188 0.086653 0.182483 v -0.163704 0.074431 0.168840 v -0.205621 0.040956 0.186602 v -0.178446 0.022905 0.166030 v -0.241430 0.025885 0.162216 v -0.209060 0.005801 0.131276 v -0.287243 0.082063 0.171647 v -0.296706 0.119775 0.162623 v -0.289092 0.106380 0.132988 v -0.283723 0.064554 0.150626 v -0.285171 0.157221 0.163468 v -0.272044 0.147988 0.129323 v -0.253327 0.169348 0.176908 v -0.236472 0.161568 0.146734 v -0.222173 0.156423 0.193075 v -0.204793 0.147337 0.171245 v -0.212710 0.118711 0.202099 v -0.199424 0.105512 0.188884 v -0.224246 0.081265 0.201254 v -0.216472 0.063904 0.192549 v -0.256090 0.069138 0.187813 v -0.252044 0.050324 0.175138 v -0.353232 0.053698 0.030965 v -0.353760 0.030469 0.037688 v -0.324269 0.031326 0.041426 v -0.323885 0.057345 0.033224 v -0.352740 0.006698 0.033129 v -0.323338 0.004550 0.035631 v -0.304853 0.010923 0.030018 v -0.299148 0.031153 0.035976 v -0.305273 0.050592 0.028161 v -0.334734 0.000697 -0.051452 v -0.323320 -0.001602 -0.015558 v -0.346810 0.011670 -0.080776 v -0.350846 0.039462 -0.091678 v -0.346922 0.064462 -0.083303 v -0.335118 0.067061 -0.054793 v -0.323967 0.061743 -0.018855 v -0.289400 0.029871 0.010808 v -0.297750 0.056382 0.010173 v -0.297168 0.003330 0.012755 v -0.321271 -0.003372 0.015053 v -0.350358 -0.000443 0.016477 v -0.615244 -0.000200 -0.010372 v -0.620331 0.008253 -0.008654 v -0.623655 0.008129 -0.020402 v -0.617745 -0.004041 -0.019251 v -0.616288 0.017266 -0.011102 v -0.619100 0.020900 -0.019696 v -0.613420 0.018537 -0.026731 v -0.616514 0.008607 -0.030293 v -0.612457 -0.000947 -0.026802 v -0.322003 0.062994 0.011807 v -0.350969 0.059212 0.013772 v -0.313442 0.072740 0.247063 v -0.322153 0.103672 0.244819 v -0.308200 0.114723 0.206012 v -0.296831 0.083134 0.207471 v -0.323616 0.128532 0.263570 v -0.301355 0.146119 0.212095 v -0.295182 0.134947 0.272333 v -0.274789 0.156327 0.224178 v -0.269272 0.128484 0.286490 v -0.246714 0.145541 0.234720 v -0.254208 0.103631 0.273425 v -0.235345 0.113952 0.236179 v -0.259551 0.072692 0.269597 v -0.242190 0.082556 0.230096 v -0.284425 0.066955 0.254185 v -0.268756 0.072348 0.218013 v -0.499435 -0.010364 0.338635 v -0.496695 -0.003966 0.345400 v -0.508832 0.000335 0.345397 v -0.509635 -0.010336 0.338243 v -0.494233 0.004552 0.341386 v -0.502229 0.010899 0.342159 v -0.507633 0.011658 0.333834 v -0.515168 0.004529 0.335002 v -0.513403 -0.004885 0.330783 v -0.308830 0.117650 0.305163 v -0.333993 0.117384 0.297161 v -0.287626 0.117555 0.317133 v -0.282376 0.011723 0.475378 v -0.284447 0.004155 0.475714 v -0.275485 0.004326 0.480332 v -0.276146 0.014657 0.479215 v -0.280861 -0.001906 0.474084 v -0.273842 -0.004720 0.477237 v -0.266777 -0.001534 0.477736 v -0.265542 0.005781 0.480738 v -0.268437 0.013585 0.479153 v -0.297477 0.059960 0.284779 v -0.276273 0.059865 0.296748 v -0.321770 0.059693 0.277516 v -0.352483 0.069385 0.260691 v -0.356374 0.094502 0.261201 v -0.337149 0.099071 0.260708 v -0.334281 0.072688 0.259948 v -0.367278 0.114018 0.273306 v -0.346364 0.119685 0.274143 v -0.379477 0.110693 0.292135 v -0.357258 0.116378 0.294771 v -0.387137 0.093672 0.307092 v -0.364444 0.098647 0.311043 v -0.384419 0.072097 0.307583 v -0.362535 0.075993 0.311410 v -0.374690 0.056124 0.296478 v -0.354279 0.059109 0.299101 v -0.361316 0.055906 0.276649 v -0.342426 0.058687 0.277347 v -0.256176 0.100143 0.296141 v -0.264397 0.120764 0.310716 v -0.255148 0.073766 0.294110 v -0.264885 0.059785 0.311037 v -0.277461 0.060227 0.333061 v -0.284948 0.077118 0.346319 v -0.285242 0.099765 0.347032 v -0.276239 0.117476 0.331422 v -0.402697 0.049273 0.242594 v -0.411900 0.066992 0.245008 v -0.383018 0.083546 0.253820 v -0.376357 0.062230 0.252914 v -0.420260 0.080184 0.257567 v -0.393624 0.099766 0.265615 v -0.420993 0.076909 0.274488 v -0.401507 0.096392 0.283394 v -0.416336 0.064149 0.286813 v -0.404091 0.081498 0.297274 v -0.408526 0.048883 0.285588 v -0.398800 0.063161 0.297376 v -0.401560 0.038145 0.274217 v -0.389565 0.049920 0.286588 v -0.399433 0.038966 0.256108 v -0.380312 0.050315 0.267802 v -0.452884 0.008712 0.240077 v -0.460655 0.025765 0.247359 v -0.437845 0.045628 0.241207 v -0.428294 0.028568 0.235709 v -0.460246 0.038980 0.262022 v -0.442046 0.058522 0.255659 v -0.448813 0.036666 0.275591 v -0.435584 0.055681 0.271357 v -0.435121 0.025064 0.282425 v -0.424903 0.043648 0.280933 v -0.427935 0.010414 0.276885 v -0.416443 0.028965 0.277018 v -0.428928 -0.000399 0.263963 v -0.413334 0.018448 0.264150 v -0.439777 -0.000488 0.248653 v -0.418704 0.018912 0.246868 v -0.393307 0.011050 -0.001450 v -0.391118 0.029331 -0.005860 v -0.369920 0.027789 -0.005358 v -0.370685 0.008147 0.000041 v -0.390651 0.047551 -0.000444 v -0.370829 0.047649 -0.000595 v -0.393086 0.052090 0.013714 v -0.373457 0.054541 0.014131 v -0.396259 0.045190 0.027911 v -0.375989 0.048275 0.029275 v -0.398003 0.029087 0.033441 v -0.376613 0.028661 0.035291 v -0.398025 0.013045 0.029145 v -0.375564 0.008829 0.031144 v -0.396035 0.006327 0.013867 v -0.373077 0.001909 0.015801 v -0.449802 0.028638 -0.005528 v -0.441104 0.043937 -0.008430 v -0.416214 0.035492 -0.006476 v -0.422244 0.019336 -0.003112 v -0.434155 0.059103 -0.001858 v -0.411905 0.051507 -0.000385 v -0.435161 0.062433 0.012012 v -0.413477 0.054970 0.013352 v -0.440888 0.056381 0.025134 v -0.418139 0.048554 0.026630 v -0.448099 0.043405 0.029462 v -0.423101 0.034931 0.031344 v -0.453561 0.030562 0.024318 v -0.426340 0.021448 0.026603 v -0.454042 0.024910 0.009021 v -0.425837 0.015453 0.011516 v -0.480800 0.037883 -0.012970 v -0.477561 0.058700 -0.017657 v -0.461685 0.052334 -0.012945 v -0.468285 0.034589 -0.009285 v -0.476331 0.079338 -0.011118 v -0.456961 0.069925 -0.006304 v -0.479041 0.083726 0.005032 v -0.458670 0.073729 0.008662 v -0.483020 0.075421 0.021064 v -0.463762 0.066682 0.023126 v -0.485597 0.057981 0.027245 v -0.469189 0.051719 0.028258 v -0.486164 0.040720 0.022201 v -0.472740 0.036910 0.023090 v -0.484116 0.032955 0.004556 v -0.472204 0.030324 0.006651 v -0.496118 -0.005903 0.293452 v -0.496392 0.007661 0.294968 v -0.480133 0.013715 0.268403 v -0.476204 -0.001695 0.263520 v -0.490065 0.018259 0.300459 v -0.476125 0.025755 0.278787 v -0.479472 0.016558 0.307218 v -0.464210 0.023823 0.288776 v -0.470737 0.007443 0.311827 v -0.452320 0.013467 0.294079 v -0.469982 -0.004204 0.310868 v -0.448385 0.000236 0.290408 v -0.475829 -0.012884 0.305933 v -0.452386 -0.009626 0.281236 v -0.486902 -0.013101 0.298617 v -0.464307 -0.009872 0.270035 v 0.214935 -0.011545 -0.036822 v 0.214193 0.023545 -0.025489 v 0.176791 0.039978 -0.011851 v 0.176669 -0.005375 -0.024981 v 0.212547 0.060925 -0.039984 v 0.176626 0.086859 -0.029612 v 0.210794 0.073386 -0.079251 v 0.176187 0.102486 -0.076721 v 0.208829 0.063248 -0.125249 v 0.175763 0.088386 -0.122286 v 0.200029 0.033257 -0.135513 v 0.175641 0.046087 -0.135416 v 0.210610 -0.001686 -0.122659 v 0.175806 0.002260 -0.117655 v 0.149762 0.058548 -0.153877 v 0.213978 -0.017134 -0.075035 v 0.176245 -0.016421 -0.070546 v 0.039811 0.010002 0.246021 v 0.024898 0.058414 0.246671 v 0.012898 0.058658 0.197938 v 0.028870 0.004908 0.197900 v 0.038258 0.106660 0.239018 v 0.028823 0.112184 0.192889 v 0.080409 0.122522 0.225396 v 0.076660 0.129727 0.184423 v 0.123077 0.106165 0.214109 v 0.124513 0.111512 0.177627 v 0.137990 0.057753 0.213459 v 0.140485 0.057763 0.177589 v 0.124630 0.009507 0.221112 v 0.124560 0.004236 0.182638 v 0.082480 -0.006355 0.234734 v 0.076722 -0.013307 0.191105 v -0.500202 0.038011 -0.016318 v -0.499007 0.059020 -0.021339 v -0.488335 0.061054 -0.020199 v -0.490454 0.039212 -0.015171 v -0.499793 0.079847 -0.015191 v -0.488270 0.082707 -0.013695 v -0.502917 0.084241 0.000799 v -0.491314 0.087290 0.002850 v -0.506068 0.075843 0.016890 v -0.494922 0.078566 0.019404 v -0.506931 0.058295 0.023341 v -0.496548 0.060300 0.025935 v -0.505813 0.040928 0.018621 v -0.496121 0.042222 0.020932 v -0.503020 0.033074 0.001202 v -0.493569 0.034064 0.002885 v -0.530227 0.025482 -0.017873 v -0.533915 0.041303 -0.022653 v -0.514573 0.052222 -0.021842 v -0.512997 0.033713 -0.016842 v -0.539125 0.056987 -0.018620 v -0.517913 0.070571 -0.016763 v -0.542543 0.060218 -0.006183 v -0.521278 0.074398 -0.002436 v -0.543169 0.053856 0.006831 v -0.523055 0.066977 0.012267 v -0.540003 0.040759 0.012599 v -0.521629 0.051584 0.018477 v -0.535314 0.027798 0.009553 v -0.518438 0.036350 0.014607 v -0.531374 0.021843 -0.003871 v -0.514923 0.029408 -0.000929 v -0.577991 0.000650 -0.024082 v -0.578647 0.014504 -0.027660 v -0.555916 0.026906 -0.024715 v -0.553283 0.012486 -0.020540 v -0.580600 0.028238 -0.023848 v -0.559923 0.041203 -0.020971 v -0.582968 0.031111 -0.013325 v -0.562857 0.044160 -0.009769 v -0.584473 0.025561 -0.002581 v -0.563693 0.038367 0.001880 v -0.583838 0.014026 0.001900 v -0.561411 0.026410 0.006963 v -0.581905 0.002611 -0.001010 v -0.557754 0.014577 0.004127 v -0.579517 -0.002581 -0.012435 v -0.554470 0.009156 -0.007983 v -0.600178 0.009568 -0.031116 v -0.600182 -0.004097 -0.027738 v -0.601447 0.023114 -0.027281 v -0.603627 0.025959 -0.016986 v -0.605354 0.020491 -0.006542 v -0.605268 0.009097 -0.002263 v -0.603909 -0.002180 -0.005197 v -0.601820 -0.007295 -0.016393 v -0.509546 0.005785 0.317680 v -0.510490 -0.007164 0.317286 v -0.502445 0.015902 0.321540 v -0.492293 0.014278 0.327223 v -0.484610 0.005577 0.331600 v -0.484915 -0.005542 0.331544 v -0.491378 -0.013828 0.328022 v -0.502168 -0.014035 0.322002 v 0.313315 0.007703 -0.058138 v 0.308716 0.036194 -0.048217 v 0.261994 0.025344 -0.037610 v 0.265288 -0.004698 -0.048134 v 0.299290 0.065066 -0.056707 v 0.255849 0.056912 -0.048506 v 0.289713 0.074690 -0.084085 v 0.250000 0.067435 -0.080696 v 0.282192 0.068344 -0.117373 v 0.244793 0.067849 -0.117902 v 0.256094 0.056557 -0.157524 v 0.261279 0.037416 -0.166642 v 0.236729 0.038862 -0.172929 v 0.237751 0.062360 -0.161267 v 0.259483 0.018081 -0.165288 v 0.239172 0.013511 -0.172180 v 0.222139 0.019557 -0.165252 v 0.215645 0.038455 -0.166805 v 0.221522 0.054665 -0.159525 v 0.298437 0.006194 -0.120665 v 0.308412 -0.000776 -0.085993 v 0.262586 -0.010639 -0.080201 v 0.255775 -0.004619 -0.119663 v 0.208388 0.033650 -0.150155 v 0.216451 0.057582 -0.147589 v 0.217960 0.010471 -0.151697 v 0.246746 0.001224 -0.152374 v 0.273989 0.008943 -0.150918 v 0.278933 0.036664 -0.146061 v 0.266241 0.063631 -0.142228 v 0.296495 0.039024 -0.127554 v 0.240575 0.069090 -0.143842 v 0.045855 0.084815 -0.269362 v 0.033227 0.048277 -0.270761 v 0.018422 0.055851 -0.211389 v 0.033213 0.101688 -0.211726 v 0.045896 0.012246 -0.265054 v 0.033257 0.010313 -0.206932 v 0.083848 -0.001521 -0.253515 v 0.077706 -0.005857 -0.199859 v 0.130864 0.005496 -0.247093 v 0.122140 0.009125 -0.194430 v 0.142162 0.048781 -0.239743 v 0.136929 0.057047 -0.194906 v 0.130449 0.091083 -0.252290 v 0.122092 0.104670 -0.199503 v 0.083791 0.100102 -0.259585 v 0.077645 0.118756 -0.206436 v 0.043045 0.044910 -0.308633 v 0.055080 0.078326 -0.307129 v 0.055118 0.012025 -0.302982 v 0.091288 -0.001006 -0.291372 v 0.133589 -0.002835 -0.284589 v 0.229401 0.004030 -0.466031 v 0.228709 0.011812 -0.473002 v 0.241554 0.012334 -0.478798 v 0.239709 0.002263 -0.470508 v 0.227656 0.020004 -0.466111 v 0.237488 0.022592 -0.470610 v 0.243198 0.020971 -0.462272 v 0.248488 0.013043 -0.468116 v 0.244943 0.004997 -0.462191 v 0.091233 0.092954 -0.297274 v 0.132770 0.092473 -0.290494 v 0.200437 0.010398 -0.296628 v 0.203611 0.035857 -0.292929 v 0.170475 0.040945 -0.269626 v 0.169646 0.009511 -0.274374 v 0.196437 0.060405 -0.299024 v 0.167321 0.071520 -0.277974 v 0.180251 0.067372 -0.314113 v 0.160958 0.080565 -0.298218 v 0.165397 0.057671 -0.328405 v 0.155370 0.068942 -0.317264 v 0.162224 0.032211 -0.332103 v 0.154540 0.037507 -0.322012 v 0.169397 0.007663 -0.326008 v 0.157695 0.006933 -0.313664 v 0.185584 0.000696 -0.310919 v 0.164058 -0.002113 -0.293419 v 0.228964 0.014758 -0.347284 v 0.233020 0.034702 -0.345572 v 0.223308 0.035835 -0.318029 v 0.219233 0.013680 -0.320632 v 0.224763 0.053856 -0.347744 v 0.214769 0.057113 -0.321909 v 0.205595 0.059188 -0.353646 v 0.195104 0.063036 -0.331847 v 0.187828 0.051488 -0.359394 v 0.176927 0.054482 -0.341360 v 0.183772 0.031544 -0.361106 v 0.172852 0.032327 -0.343963 v 0.192029 0.012390 -0.358934 v 0.181391 0.011049 -0.340083 v 0.211197 0.007058 -0.353032 v 0.201056 0.005126 -0.330145 v 0.235287 0.002638 -0.409834 v 0.238334 0.017092 -0.408770 v 0.236197 0.026281 -0.376208 v 0.232561 0.009033 -0.377478 v 0.232191 0.030974 -0.409976 v 0.228867 0.042846 -0.377648 v 0.217891 0.034838 -0.413405 v 0.211802 0.047458 -0.381739 v 0.204622 0.029257 -0.416786 v 0.195969 0.040798 -0.385775 v 0.201575 0.014804 -0.417850 v 0.192333 0.023550 -0.387044 v 0.207718 0.000922 -0.416644 v 0.199663 0.006985 -0.385605 v 0.222018 -0.002943 -0.413215 v 0.216728 0.002373 -0.381513 v -0.195709 -0.000531 -0.508181 v -0.190023 0.020406 -0.510858 v -0.165175 0.024215 -0.468430 v -0.171859 -0.000396 -0.465283 v -0.195709 0.041343 -0.508181 v -0.171859 0.048826 -0.465283 v -0.212769 0.048322 -0.500151 v -0.191912 0.057029 -0.455844 v -0.229828 0.041343 -0.492121 v -0.211965 0.048826 -0.446404 v -0.235515 0.020406 -0.489444 v -0.218649 0.024215 -0.443258 v -0.229828 -0.000531 -0.492121 v -0.211965 -0.000396 -0.446404 v -0.212769 -0.007510 -0.500151 v -0.191912 -0.008600 -0.455844 v -0.210797 0.021168 -0.551537 v -0.216151 0.001456 -0.549017 v -0.216151 0.040881 -0.549017 v -0.232212 0.047451 -0.541456 v -0.248274 0.040881 -0.533895 v -0.253628 0.021168 -0.531375 v -0.248274 0.001456 -0.533895 v -0.232212 -0.005115 -0.541456 v -0.260902 0.007780 0.475240 v -0.265090 0.018120 0.475280 v -0.263024 -0.003003 0.473533 v -0.272255 -0.006071 0.470834 v -0.282120 -0.002656 0.468673 v -0.286145 0.006007 0.468575 v -0.283859 0.015114 0.470143 v -0.274792 0.019858 0.472980 v 0.072525 0.054676 0.333097 v 0.053770 0.086720 0.317159 v 0.042676 0.071424 0.287076 v 0.058050 0.030078 0.289776 v 0.060277 0.129018 0.310931 v 0.051970 0.113229 0.275939 v 0.098755 0.140408 0.294198 v 0.087957 0.127774 0.260978 v 0.132187 0.130110 0.286796 v 0.125971 0.114603 0.250629 v 0.147306 0.095469 0.293342 v 0.141345 0.073257 0.253329 v 0.141130 0.059893 0.307075 v 0.132052 0.031452 0.264465 v 0.110679 0.046787 0.321236 v 0.096064 0.016906 0.279427 v 0.013864 0.068704 0.405022 v 0.001870 0.078760 0.406784 v -0.001984 0.078039 0.391862 v 0.010169 0.069730 0.389591 v 0.000004 0.095434 0.406493 v -0.004644 0.092533 0.389678 v -0.002777 0.088830 0.376945 v -0.001167 0.077564 0.375950 v 0.009391 0.072528 0.376940 v 0.091257 0.154626 0.368762 v 0.123189 0.158347 0.356935 v 0.112254 0.155349 0.328085 v 0.077898 0.150945 0.341658 v 0.139850 0.147914 0.346302 v 0.140718 0.145966 0.322220 v 0.148551 0.130015 0.358216 v 0.152631 0.115588 0.329643 v 0.141917 0.104535 0.361341 v 0.146173 0.084652 0.343320 v 0.124203 0.101623 0.376707 v 0.119528 0.073596 0.356217 v 0.094828 0.100003 0.386024 v 0.087984 0.073681 0.366252 v -0.244224 0.021633 -0.208208 v 0.300438 0.113530 0.051428 v 0.260233 0.106938 0.041435 v 0.289195 0.078272 0.062749 v 0.257812 0.065875 0.056649 v 0.275094 0.067742 0.094898 v 0.245203 0.052256 0.094618 v 0.265901 0.081024 0.126594 v 0.231258 0.066030 0.130669 v 0.269379 0.117200 0.136554 v 0.224827 0.107144 0.140129 v 0.382747 0.060207 0.101125 v 0.378729 0.036589 0.094456 v 0.341191 0.078779 0.071799 v 0.347017 0.108442 0.080499 v 0.369645 0.013795 0.101511 v 0.329006 0.050152 0.080409 v 0.359863 0.007298 0.122162 v 0.316466 0.041991 0.106361 v 0.354448 0.016270 0.142683 v 0.309929 0.053261 0.132342 v 0.358466 0.039888 0.149352 v 0.315754 0.082924 0.141042 v 0.367550 0.062682 0.142297 v 0.327939 0.111550 0.132431 v 0.377332 0.069179 0.121646 v 0.340480 0.119711 0.106480 v 0.436351 0.037635 0.129290 v 0.437004 0.020240 0.125883 v 0.409289 0.020862 0.111307 v 0.410751 0.040946 0.116164 v 0.433913 0.003454 0.132240 v 0.403511 0.001479 0.117934 v 0.427890 -0.001332 0.147378 v 0.395831 -0.004046 0.135456 v 0.422679 0.005276 0.161532 v 0.390566 0.003584 0.152388 v 0.422026 0.022671 0.164939 v 0.392028 0.023668 0.157245 v 0.425117 0.039458 0.158582 v 0.397806 0.043050 0.150617 v 0.431140 0.044243 0.143444 v 0.405485 0.048576 0.133095 v 0.466009 0.023366 0.141717 v 0.464866 0.037250 0.144177 v 0.464162 0.009968 0.146991 v 0.459558 0.006148 0.159061 v 0.455188 0.011422 0.170193 v 0.454045 0.025306 0.172653 v 0.455892 0.038704 0.167379 v 0.460496 0.042524 0.155309 v -0.346505 0.009389 -0.411818 v -0.348371 0.000808 -0.409511 v -0.348224 0.018115 -0.410425 v -0.353585 0.020309 -0.404979 v -0.358991 0.017187 -0.399252 v -0.360845 0.009967 -0.397016 v -0.359115 0.002602 -0.398481 v -0.353766 -0.000953 -0.403855 v -0.422209 0.028613 -0.279045 v -0.418633 0.041197 -0.280535 v -0.392615 0.029587 -0.269345 v -0.397456 0.015199 -0.266530 v -0.419219 0.051924 -0.273295 v -0.394831 0.043081 -0.263469 v -0.424963 0.053025 -0.259243 v -0.404981 0.046385 -0.249923 v -0.431704 0.046355 -0.247107 v -0.416006 0.040394 -0.237398 v -0.435280 0.033771 -0.245616 v -0.420847 0.026006 -0.234584 v -0.434694 0.023044 -0.252856 v -0.418630 0.012512 -0.240460 v -0.428950 0.021943 -0.266909 v -0.408481 0.009209 -0.254005 v -0.459678 0.039994 -0.291077 v -0.460748 0.049158 -0.293135 v -0.441903 0.049785 -0.287853 v -0.443145 0.038957 -0.286408 v -0.464000 0.056699 -0.288367 v -0.443401 0.058626 -0.281389 v -0.467993 0.057049 -0.277677 v -0.447555 0.058922 -0.268688 v -0.470547 0.051830 -0.267890 v -0.451622 0.052662 -0.257661 v -0.469477 0.042666 -0.265832 v -0.452864 0.041834 -0.256216 v -0.466226 0.035125 -0.270600 v -0.451366 0.032993 -0.262681 v -0.462232 0.034776 -0.281290 v -0.447213 0.032697 -0.275381 v -0.483774 0.008588 -0.299925 v -0.485970 0.015128 -0.302894 v -0.473493 0.033123 -0.298215 v -0.471227 0.025490 -0.295506 v -0.489914 0.021028 -0.300334 v -0.477634 0.039660 -0.294849 v -0.493562 0.022142 -0.292110 v -0.481516 0.040378 -0.285626 v -0.495163 0.019110 -0.283750 v -0.483261 0.036373 -0.276622 v -0.492968 0.012571 -0.280781 v -0.480995 0.028741 -0.273913 v -0.489023 0.006671 -0.283341 v -0.476854 0.022204 -0.277280 v -0.485375 0.005556 -0.291565 v -0.472973 0.021486 -0.286503 v -0.525887 0.001769 -0.309852 v -0.524980 0.007814 -0.311495 v -0.504009 0.008621 -0.306968 v -0.503304 0.002432 -0.304560 v -0.525791 0.013382 -0.308536 v -0.506447 0.014323 -0.304324 v -0.528351 0.014602 -0.301414 v -0.509572 0.015572 -0.296707 v -0.530944 0.011951 -0.294731 v -0.511649 0.012858 -0.289169 v -0.531850 0.005906 -0.293089 v -0.510944 0.006668 -0.286760 v -0.531039 0.000338 -0.296048 v -0.508506 0.000967 -0.289404 v -0.528479 -0.000882 -0.303169 v -0.505381 -0.000283 -0.297021 v -0.546254 0.006916 -0.317531 v -0.547594 0.001350 -0.316243 v -0.546503 0.012042 -0.314687 v -0.548705 0.013166 -0.308230 v -0.551270 0.010725 -0.302292 v -0.552610 0.005159 -0.301004 v -0.552361 0.000033 -0.303848 v -0.550160 -0.001091 -0.310305 v 0.370419 0.023367 -0.075609 v 0.372562 0.051255 -0.068471 v 0.342878 0.046915 -0.057313 v 0.344601 0.018195 -0.066053 v 0.369436 0.079142 -0.078055 v 0.335627 0.075636 -0.066117 v 0.361368 0.088438 -0.103546 v 0.325840 0.085209 -0.092441 v 0.353628 0.079142 -0.128222 v 0.319044 0.075636 -0.118745 v 0.351485 0.051255 -0.135360 v 0.320767 0.046915 -0.127485 v 0.354612 0.023367 -0.125776 v 0.328018 0.018195 -0.118681 v 0.362680 0.014071 -0.100285 v 0.337805 0.008622 -0.092357 v 0.437697 0.029993 -0.102297 v 0.405849 0.042958 -0.085263 v 0.402043 0.019808 -0.090538 v 0.438221 0.047716 -0.109207 v 0.405289 0.066109 -0.093844 v 0.433912 0.053623 -0.125740 v 0.399281 0.073825 -0.115178 v 0.428133 0.047716 -0.141223 v 0.392192 0.066109 -0.135409 v 0.424246 0.029993 -0.144985 v 0.388386 0.042958 -0.140684 v 0.388945 0.019808 -0.132103 v 0.394953 0.012091 -0.110769 v 0.463067 0.020325 -0.114179 v 0.463266 0.035145 -0.119869 v 0.459591 0.040085 -0.133637 v 0.454849 0.035145 -0.146580 v 0.451844 0.020325 -0.149793 v 0.126298 0.135156 0.375045 v 0.096410 0.132315 0.385206 v 0.040450 0.078470 0.355322 v 0.028149 0.092307 0.347250 v 0.045716 0.093333 0.334926 v 0.056951 0.073328 0.347902 v 0.020092 0.110493 0.352477 v 0.040408 0.120704 0.336663 v 0.023908 0.125011 0.372161 v 0.048308 0.139963 0.357362 v 0.033109 0.129285 0.398931 v 0.060838 0.143273 0.381555 v 0.050742 0.110239 0.403510 v 0.069423 0.122799 0.393776 v 0.054113 0.083847 0.399035 v 0.072082 0.094959 0.391285 v 0.049367 0.077001 0.375534 v 0.066832 0.076169 0.371340 v 0.010699 0.084685 0.361002 v 0.021528 0.074605 0.366510 v 0.003022 0.097626 0.366469 v 0.005122 0.108695 0.383023 v 0.011011 0.113435 0.403860 v 0.031955 0.017637 0.535588 v 0.032446 0.010980 0.537631 v 0.040292 0.015143 0.540174 v 0.036680 0.023431 0.537843 v 0.036335 0.006818 0.534564 v 0.043259 0.007180 0.536305 v 0.047975 0.012008 0.533624 v 0.047493 0.019631 0.536517 v 0.042608 0.025264 0.534878 v 0.028883 0.072119 0.382883 v 0.033242 0.070559 0.401500 v 0.034047 0.098694 0.438627 v 0.046931 0.083665 0.432827 v 0.045768 0.098267 0.417161 v 0.031616 0.114371 0.421059 v 0.049791 0.062797 0.426479 v 0.049482 0.075689 0.413662 v 0.035865 0.051509 0.424803 v 0.035082 0.063257 0.413741 v 0.017448 0.050460 0.426591 v 0.015588 0.061856 0.415929 v 0.006838 0.060306 0.430637 v 0.004016 0.072376 0.418759 v 0.006251 0.075991 0.435231 v 0.002882 0.089368 0.421190 v 0.017904 0.092462 0.438662 v 0.014701 0.107386 0.422179 v 0.037685 0.053362 0.471306 v 0.046178 0.043490 0.466755 v 0.046660 0.063221 0.448605 v 0.035968 0.075700 0.454369 v 0.047932 0.029812 0.461549 v 0.048868 0.045929 0.442007 v 0.038545 0.022444 0.459878 v 0.037049 0.036613 0.439886 v 0.026235 0.021793 0.460989 v 0.021550 0.035789 0.441290 v 0.019222 0.028265 0.464131 v 0.012720 0.043970 0.445269 v 0.018948 0.038543 0.467928 v 0.012375 0.056963 0.450082 v 0.026856 0.049311 0.471008 v 0.022332 0.070577 0.453988 v 0.041242 0.032451 0.513003 v 0.047806 0.024254 0.511460 v 0.046707 0.031033 0.489537 v 0.039510 0.039656 0.492458 v 0.049182 0.013086 0.510143 v 0.048203 0.019173 0.486406 v 0.041961 0.007264 0.510280 v 0.040264 0.012877 0.485666 v 0.032475 0.006966 0.511234 v 0.029846 0.012422 0.486651 v 0.027057 0.012366 0.512364 v 0.023905 0.018088 0.488699 v 0.026827 0.020739 0.513268 v 0.023664 0.026990 0.490956 v 0.032902 0.029356 0.513544 v 0.030347 0.036244 0.492570 v 0.049033 0.021561 0.527033 v 0.042680 0.029615 0.528117 v 0.050369 0.010623 0.526320 v 0.043387 0.004960 0.526750 v 0.034211 0.004713 0.527693 v 0.028969 0.010025 0.528518 v 0.028742 0.018221 0.528971 v 0.034615 0.026627 0.528801 v 0.244923 0.013654 -0.442090 v 0.242404 0.001708 -0.442969 v 0.239846 0.025128 -0.443086 v 0.228026 0.028322 -0.445920 v 0.217060 0.023709 -0.448715 v 0.214542 0.011763 -0.449595 v 0.219618 0.000290 -0.448598 v 0.231438 -0.002904 -0.445764 vt 0.770437 0.582312 vt 0.770299 0.531964 vt 0.878209 0.536342 vt 0.683696 0.536342 vt 0.736189 0.502710 vt 0.776974 0.490852 vt 0.814925 0.505853 vt 0.456331 0.582312 vt 0.456503 0.531964 vt 0.531950 0.536342 vt 0.364131 0.536342 vt 0.416867 0.496199 vt 0.455369 0.488217 vt 0.487438 0.504053 vt 0.605766 0.582312 vt 0.605571 0.531964 vt 0.571832 0.505407 vt 0.604738 0.489421 vt 0.640071 0.500720 vt 0.248184 0.582312 vt 0.250719 0.531964 vt 0.102799 0.536342 vt 0.196845 0.503757 vt 0.254919 0.491381 vt 0.299896 0.505710 vt 0.745049 0.465743 vt 0.745645 0.448066 vt 0.759108 0.448703 vt 0.758187 0.431633 vt 0.768750 0.436414 vt 0.772993 0.448515 vt 0.769210 0.461049 vt 0.758941 0.466114 vt 0.572323 0.440821 vt 0.574536 0.441110 vt 0.574854 0.445950 vt 0.574327 0.435115 vt 0.576855 0.440244 vt 0.356366 0.497343 vt 0.365547 0.460043 vt 0.404247 0.458954 vt 0.404642 0.431490 vt 0.443352 0.458113 vt 0.201498 0.454066 vt 0.200358 0.445641 vt 0.212216 0.443618 vt 0.202194 0.436082 vt 0.211430 0.432205 vt 0.223420 0.441458 vt 0.225269 0.452937 vt 0.210493 0.454956 vt 0.429003 0.918625 vt 0.426172 0.912411 vt 0.438158 0.904419 vt 0.429362 0.918625 vt 0.438270 0.912411 vt 0.447120 0.918625 vt 0.450145 0.912411 vt 0.446954 0.918625 vt 0.438046 0.912411 vt 0.528346 0.427286 vt 0.504652 0.428001 vt 0.532405 0.428582 vt 0.459382 0.427927 vt 0.478990 0.428285 vt 0.592258 0.428285 vt 0.598761 0.427927 vt 0.553135 0.428001 vt 0.848886 0.672062 vt 0.777877 0.672062 vt 0.770483 0.628282 vt 0.683775 0.628282 vt 0.683756 0.583406 vt 0.873338 0.583406 vt 0.869730 0.628282 vt 0.605831 0.672062 vt 0.605831 0.628282 vt 0.532070 0.628282 vt 0.532040 0.583406 vt 0.456273 0.672062 vt 0.456273 0.628282 vt 0.364657 0.628282 vt 0.364523 0.583406 vt 0.364657 0.672062 vt 0.247234 0.672062 vt 0.247234 0.628282 vt 0.099233 0.628282 vt 0.100128 0.583406 vt 0.783242 0.755781 vt 0.770483 0.755781 vt 0.770483 0.715843 vt 0.683775 0.755781 vt 0.683775 0.715843 vt 0.832338 0.715843 vt 0.605831 0.755781 vt 0.605831 0.715843 vt 0.532070 0.755781 vt 0.532070 0.715843 vt 0.683775 0.672062 vt 0.456273 0.755781 vt 0.456273 0.715843 vt 0.364657 0.715843 vt 0.364657 0.755781 vt 0.247234 0.755781 vt 0.247234 0.715843 vt 0.099233 0.715843 vt 0.099233 0.672062 vt 0.753143 0.811133 vt 0.698666 0.808948 vt 0.770483 0.788033 vt 0.683775 0.788033 vt 0.778743 0.788033 vt 0.605831 0.808948 vt 0.605831 0.788033 vt 0.532070 0.808948 vt 0.532070 0.788033 vt 0.456273 0.808948 vt 0.456273 0.788033 vt 0.364657 0.788033 vt 0.364657 0.808948 vt 0.247234 0.808948 vt 0.247234 0.788033 vt 0.099233 0.788033 vt 0.454140 0.936383 vt 0.450256 0.936383 vt 0.698666 0.814873 vt 0.683775 0.814873 vt 0.753143 0.816718 vt 0.438307 0.936383 vt 0.426283 0.936383 vt 0.605831 0.814873 vt 0.532070 0.814873 vt 0.422176 0.936383 vt 0.426060 0.936383 vt 0.456273 0.814873 vt 0.364657 0.814873 vt 0.438009 0.936383 vt 0.457082 0.935935 vt 0.104309 0.811133 vt 0.247234 0.814873 vt 0.527545 0.425072 vt 0.497833 0.425072 vt 0.494388 0.421847 vt 0.454888 0.427515 vt 0.455519 0.425072 vt 0.680179 0.427286 vt 0.683853 0.425072 vt 0.647186 0.425072 vt 0.646639 0.421466 vt 0.600610 0.425660 vt 0.600136 0.425072 vt 0.650325 0.428001 vt 0.107102 0.427286 vt 0.111560 0.425072 vt 0.049078 0.425072 vt 0.049529 0.424847 vt 0.773139 0.426926 vt 0.767160 0.425072 vt 0.841808 0.425072 vt 0.759513 0.427927 vt 0.846146 0.428001 vt 0.365887 0.427286 vt 0.360849 0.425072 vt 0.319389 0.425072 vt 0.316604 0.424184 vt 0.260659 0.426466 vt 0.259372 0.425072 vt 0.334728 0.428001 vt 0.070552 0.438367 vt 0.066949 0.438740 vt 0.066736 0.428722 vt 0.069431 0.445222 vt 0.066736 0.448012 vt 0.063711 0.445222 vt 0.062941 0.438367 vt 0.564438 0.501489 vt 0.576778 0.506641 vt 0.572024 0.506073 vt 0.587872 0.499333 vt 0.586376 0.490779 vt 0.565341 0.497781 vt 0.555700 0.499409 vt 0.390414 0.428582 vt 0.264894 0.427927 vt 0.325801 0.428285 vt 0.401231 0.428001 vt 0.404054 0.425072 vt 0.716021 0.425072 vt 0.723030 0.422758 vt 0.705717 0.428001 vt 0.647933 0.427184 vt 0.648795 0.431842 vt 0.646254 0.431689 vt 0.648144 0.436738 vt 0.646287 0.438291 vt 0.644191 0.436738 vt 0.643503 0.431842 vt 0.643975 0.427184 vt 0.646002 0.425393 vt 0.341082 0.456398 vt 0.341612 0.464662 vt 0.335350 0.464704 vt 0.341597 0.475744 vt 0.335155 0.475787 vt 0.328830 0.464568 vt 0.327703 0.456306 vt 0.334400 0.456441 vt 0.664738 0.428582 vt 0.715598 0.428285 vt 0.560542 0.427839 vt 0.558072 0.425072 vt 0.559030 0.423722 vt 0.112645 0.426727 vt 0.178478 0.425072 vt 0.160151 0.428001 vt 0.186633 0.422179 vt 0.053415 0.428001 vt 0.083724 0.428582 vt 0.876455 0.428582 vt 0.811519 0.504157 vt 0.794788 0.470602 vt 0.786098 0.464973 vt 0.782337 0.489419 vt 0.780025 0.459275 vt 0.787462 0.451857 vt 0.795094 0.491269 vt 0.793452 0.498158 vt 0.792254 0.498051 vt 0.073071 0.491269 vt 0.080079 0.489419 vt 0.049905 0.497567 vt 0.109190 0.496573 vt 0.054992 0.505853 vt 0.079895 0.470602 vt 0.089896 0.464973 vt 0.073210 0.449935 vt 0.080869 0.440528 vt 0.111629 0.459581 vt 0.052941 0.443046 vt 0.050425 0.432379 vt 0.843156 0.432379 vt 0.827601 0.443046 vt 0.842259 0.424847 vt 0.047294 0.474018 vt 0.039418 0.475449 vt 0.043861 0.458028 vt 0.050751 0.489202 vt 0.043861 0.492869 vt 0.002363 0.491269 vt 0.002057 0.470602 vt 0.061361 0.494263 vt 0.057685 0.498675 vt 0.072026 0.492869 vt 0.043849 0.498158 vt 0.072217 0.489202 vt 0.075864 0.474018 vt 0.076862 0.475449 vt 0.072026 0.458028 vt 0.061361 0.453772 vt 0.057685 0.452222 vt 0.050751 0.458833 vt 0.018788 0.504157 vt 0.052073 0.445749 vt 0.050175 0.460813 vt 0.053111 0.446374 vt 0.054600 0.459588 vt 0.053111 0.475252 vt 0.062289 0.464202 vt 0.062075 0.480065 vt 0.071206 0.475252 vt 0.070100 0.459588 vt 0.072721 0.445749 vt 0.074271 0.460813 vt 0.071206 0.446374 vt 0.072217 0.458833 vt 0.070100 0.431909 vt 0.062289 0.427296 vt 0.062075 0.441561 vt 0.054600 0.431909 vt 0.218794 0.473211 vt 0.229362 0.451397 vt 0.240984 0.455445 vt 0.230442 0.430141 vt 0.258499 0.459192 vt 0.226999 0.480748 vt 0.221456 0.429752 vt 0.194398 0.422639 vt 0.191249 0.421706 vt 0.164288 0.426690 vt 0.152127 0.430141 vt 0.153627 0.450661 vt 0.139380 0.455445 vt 0.161934 0.474630 vt 0.149318 0.480748 vt 0.191029 0.480831 vt 0.186789 0.489183 vt 0.219025 0.456712 vt 0.224649 0.442249 vt 0.222946 0.446272 vt 0.217083 0.429231 vt 0.215108 0.463990 vt 0.220617 0.427393 vt 0.202112 0.425537 vt 0.197827 0.423961 vt 0.176355 0.423550 vt 0.648130 0.427091 vt 0.645098 0.424874 vt 0.644536 0.427168 vt 0.642190 0.427091 vt 0.641194 0.429585 vt 0.643820 0.430004 vt 0.648037 0.429585 vt 0.200314 0.463327 vt 0.195366 0.471291 vt 0.174244 0.470273 vt 0.143934 0.437203 vt 0.151752 0.443329 vt 0.154639 0.425093 vt 0.144118 0.452733 vt 0.153085 0.460929 vt 0.146316 0.455351 vt 0.158658 0.463034 vt 0.163790 0.454651 vt 0.148111 0.449033 vt 0.152240 0.436810 vt 0.165744 0.440788 vt 0.164820 0.427560 vt 0.184450 0.428955 vt 0.181678 0.444760 vt 0.147738 0.418967 vt 0.160361 0.421082 vt 0.145423 0.421652 vt 0.131219 0.426982 vt 0.129635 0.437058 vt 0.136382 0.436040 vt 0.130378 0.446701 vt 0.135670 0.450369 vt 0.136897 0.421844 vt 0.127453 0.449279 vt 0.135346 0.453002 vt 0.135079 0.450625 vt 0.126627 0.437512 vt 0.133143 0.435734 vt 0.133053 0.444235 vt 0.126512 0.428961 vt 0.133529 0.427342 vt 0.139292 0.436750 vt 0.128442 0.425088 vt 0.136681 0.419610 vt 0.136117 0.422285 vt 0.642160 0.432301 vt 0.643219 0.438749 vt 0.641377 0.433740 vt 0.642918 0.425852 vt 0.646000 0.440899 vt 0.648691 0.438749 vt 0.648454 0.440389 vt 0.645542 0.442606 vt 0.642532 0.440389 vt 0.649462 0.432301 vt 0.648401 0.425852 vt 0.649285 0.433740 vt 0.645605 0.423702 vt 0.148198 0.435321 vt 0.146971 0.435352 vt 0.143684 0.445940 vt 0.144367 0.425273 vt 0.149001 0.424021 vt 0.134838 0.422756 vt 0.135503 0.422819 vt 0.126253 0.427141 vt 0.123970 0.426238 vt 0.120522 0.438327 vt 0.122844 0.437391 vt 0.125563 0.447131 vt 0.123162 0.437513 vt 0.133989 0.449577 vt 0.134583 0.449924 vt 0.156972 0.436846 vt 0.159593 0.435992 vt 0.157349 0.445110 vt 0.155239 0.428772 vt 0.157864 0.426639 vt 0.158027 0.428648 vt 0.160089 0.435951 vt 0.157610 0.443031 vt 0.134127 0.423640 vt 0.143211 0.423080 vt 0.125499 0.425209 vt 0.110320 0.431413 vt 0.108826 0.431215 vt 0.108694 0.427823 vt 0.108553 0.434813 vt 0.106933 0.431223 vt 0.126026 0.446191 vt 0.133398 0.448265 vt 0.142505 0.447708 vt 0.123243 0.449188 vt 0.124761 0.450325 vt 0.592138 0.468346 vt 0.595444 0.462415 vt 0.588113 0.487734 vt 0.584489 0.445555 vt 0.585608 0.436805 vt 0.603038 0.456694 vt 0.565012 0.437482 vt 0.561461 0.427881 vt 0.539970 0.435933 vt 0.546338 0.444484 vt 0.539558 0.466918 vt 0.532825 0.461252 vt 0.539065 0.486862 vt 0.525807 0.458579 vt 0.545054 0.489708 vt 0.562312 0.495786 vt 0.528519 0.496367 vt 0.362359 0.453941 vt 0.364480 0.444636 vt 0.358388 0.458713 vt 0.357711 0.446815 vt 0.350754 0.458925 vt 0.350986 0.471579 vt 0.356988 0.468693 vt 0.363486 0.434196 vt 0.358920 0.430249 vt 0.354707 0.441449 vt 0.354179 0.432360 vt 0.351713 0.442798 vt 0.349573 0.452952 vt 0.350126 0.452453 vt 0.352343 0.440098 vt 0.350606 0.451042 vt 0.353450 0.448971 vt 0.351314 0.461204 vt 0.349431 0.461212 vt 0.357699 0.454486 vt 0.353981 0.468306 vt 0.350193 0.480338 vt 0.349636 0.472063 vt 0.366188 0.438452 vt 0.367248 0.432751 vt 0.366752 0.436690 vt 0.365851 0.428611 vt 0.365072 0.444187 vt 0.366542 0.426779 vt 0.363932 0.425043 vt 0.362055 0.425966 vt 0.361240 0.426890 vt 0.358083 0.428102 vt 0.360236 0.431664 vt 0.356544 0.434363 vt 0.357523 0.441206 vt 0.360970 0.436710 vt 0.363519 0.439372 vt 0.361168 0.445086 vt 0.154533 0.435625 vt 0.154032 0.425575 vt 0.153438 0.445623 vt 0.151829 0.423207 vt 0.149626 0.425991 vt 0.141539 0.427069 vt 0.157132 0.429239 vt 0.156041 0.429433 vt 0.155770 0.427123 vt 0.155929 0.431950 vt 0.154570 0.429833 vt 0.151135 0.447835 vt 0.149301 0.447433 vt 0.141158 0.445218 vt 0.108394 0.432474 vt 0.106484 0.443052 vt 0.113339 0.441540 vt 0.114282 0.452332 vt 0.114881 0.430682 vt 0.107878 0.453645 vt 0.112609 0.456086 vt 0.117757 0.454705 vt 0.121143 0.450430 vt 0.118924 0.443111 vt 0.122416 0.441274 vt 0.117679 0.434183 vt 0.121622 0.432185 vt 0.113239 0.430077 vt 0.118474 0.428109 vt 0.104321 0.433529 vt 0.104158 0.438883 vt 0.105687 0.430650 vt 0.105088 0.439137 vt 0.105319 0.447173 vt 0.107711 0.440519 vt 0.109250 0.449148 vt 0.113205 0.446047 vt 0.117250 0.452024 vt 0.110403 0.438479 vt 0.111388 0.433821 vt 0.114636 0.439114 vt 0.113520 0.432123 vt 0.110610 0.429089 vt 0.108006 0.426830 vt 0.109706 0.428850 vt 0.105324 0.427994 vt 0.284615 0.469527 vt 0.274564 0.463414 vt 0.287459 0.438177 vt 0.287640 0.491886 vt 0.279977 0.488626 vt 0.309576 0.499260 vt 0.309256 0.496995 vt 0.334580 0.491709 vt 0.339557 0.488548 vt 0.343068 0.469290 vt 0.349173 0.463310 vt 0.336466 0.446931 vt 0.341436 0.438098 vt 0.315962 0.439556 vt 0.315707 0.429729 vt 0.294787 0.447108 vt 0.305947 0.467044 vt 0.300676 0.485497 vt 0.292138 0.478942 vt 0.294940 0.499301 vt 0.299807 0.458477 vt 0.303812 0.503819 vt 0.317104 0.509753 vt 0.312021 0.505946 vt 0.332281 0.503429 vt 0.332051 0.498983 vt 0.338203 0.484976 vt 0.339532 0.478518 vt 0.333877 0.466654 vt 0.334163 0.458159 vt 0.319658 0.460720 vt 0.316929 0.451514 vt 0.241967 0.453165 vt 0.244605 0.441799 vt 0.248665 0.442219 vt 0.242866 0.430168 vt 0.246231 0.429117 vt 0.245356 0.432235 vt 0.248737 0.442134 vt 0.244444 0.451645 vt 0.245129 0.454950 vt 0.207516 0.427232 vt 0.223258 0.426107 vt 0.196026 0.432601 vt 0.192033 0.446200 vt 0.195017 0.458432 vt 0.206093 0.459704 vt 0.221754 0.457102 vt 0.237172 0.441507 vt 0.237638 0.428520 vt 0.236325 0.454478 vt 0.237232 0.425241 vt 0.236275 0.426674 vt 0.226230 0.429679 vt 0.223242 0.426793 vt 0.223600 0.430929 vt 0.221018 0.430868 vt 0.223077 0.435339 vt 0.221174 0.437117 vt 0.219610 0.435961 vt 0.218830 0.431102 vt 0.219592 0.426427 vt 0.221272 0.424914 vt 0.235726 0.457714 vt 0.235139 0.455863 vt 0.317642 0.477618 vt 0.310974 0.483025 vt 0.314156 0.467568 vt 0.314293 0.498387 vt 0.330311 0.492921 vt 0.324415 0.503382 vt 0.335204 0.498104 vt 0.341516 0.477598 vt 0.339101 0.482648 vt 0.335483 0.467286 vt 0.329078 0.459652 vt 0.324494 0.462291 vt 0.305555 0.421820 vt 0.307078 0.424950 vt 0.305440 0.427055 vt 0.306735 0.429118 vt 0.305777 0.432224 vt 0.303634 0.432595 vt 0.302849 0.429107 vt 0.302353 0.424500 vt 0.304125 0.421833 vt 0.333586 0.484457 vt 0.326336 0.484327 vt 0.340892 0.484411 vt 0.339934 0.489758 vt 0.321615 0.489782 vt 0.363123 0.432627 vt 0.362727 0.428924 vt 0.365036 0.429008 vt 0.363310 0.425958 vt 0.365083 0.424581 vt 0.366619 0.426140 vt 0.367160 0.429720 vt 0.366401 0.433538 vt 0.364791 0.434062 vt 0.332325 0.456229 vt 0.340051 0.456183 vt 0.325054 0.456099 vt 0.320197 0.462482 vt 0.339213 0.462459 vt 0.314051 0.473131 vt 0.317970 0.475366 vt 0.318425 0.462457 vt 0.318946 0.485453 vt 0.314523 0.482680 vt 0.316083 0.481053 vt 0.320964 0.483834 vt 0.322696 0.475159 vt 0.328457 0.475652 vt 0.317594 0.472725 vt 0.318193 0.462168 vt 0.323145 0.464074 vt 0.317864 0.454352 vt 0.322431 0.455813 vt 0.316414 0.454246 vt 0.320461 0.455606 vt 0.314731 0.460841 vt 0.345339 0.475891 vt 0.345720 0.485980 vt 0.345250 0.462985 vt 0.345646 0.456144 vt 0.346130 0.456360 vt 0.346414 0.464625 vt 0.346455 0.475706 vt 0.346168 0.484372 vt 0.350784 0.481641 vt 0.300250 0.459670 vt 0.307256 0.467770 vt 0.308288 0.457340 vt 0.301671 0.466125 vt 0.307941 0.475707 vt 0.305148 0.464523 vt 0.310291 0.474055 vt 0.312629 0.466768 vt 0.308444 0.458279 vt 0.309521 0.450810 vt 0.313583 0.457795 vt 0.313084 0.451317 vt 0.308376 0.445555 vt 0.304838 0.445957 vt 0.310845 0.451510 vt 0.293406 0.439498 vt 0.295340 0.449216 vt 0.295542 0.440869 vt 0.296562 0.445964 vt 0.297871 0.455526 vt 0.301235 0.451000 vt 0.301020 0.444832 vt 0.302169 0.454136 vt 0.305837 0.448248 vt 0.304507 0.439155 vt 0.304519 0.431986 vt 0.306424 0.441063 vt 0.304229 0.435917 vt 0.301660 0.426695 vt 0.296694 0.426652 vt 0.299564 0.436144 vt 0.227923 0.432298 vt 0.226357 0.441242 vt 0.226903 0.440488 vt 0.228733 0.450206 vt 0.228983 0.430877 vt 0.228344 0.450157 vt 0.233412 0.452379 vt 0.234311 0.453578 vt 0.239911 0.450512 vt 0.238347 0.449002 vt 0.240205 0.441123 vt 0.242116 0.440915 vt 0.238696 0.433274 vt 0.240634 0.431211 vt 0.233362 0.429987 vt 0.234964 0.427825 vt 0.224857 0.448389 vt 0.225783 0.444257 vt 0.226833 0.436352 vt 0.227061 0.455810 vt 0.227937 0.452093 vt 0.231526 0.457439 vt 0.232602 0.453788 vt 0.236910 0.450648 vt 0.235529 0.454478 vt 0.236610 0.448129 vt 0.238254 0.443983 vt 0.236561 0.437386 vt 0.230120 0.439079 vt 0.231620 0.434452 vt 0.225680 0.440903 vt 0.223186 0.445427 vt 0.221832 0.455613 vt 0.223295 0.452498 vt 0.225360 0.461105 vt 0.224352 0.443815 vt 0.223752 0.465711 vt 0.228436 0.467858 vt 0.229909 0.462966 vt 0.232972 0.463795 vt 0.234138 0.459518 vt 0.234659 0.455261 vt 0.235484 0.452197 vt 0.233210 0.446815 vt 0.233853 0.444951 vt 0.228213 0.443016 vt 0.229033 0.441728 vt 0.234830 0.441845 vt 0.297880 0.424002 vt 0.298130 0.430639 vt 0.295123 0.433602 vt 0.297741 0.439493 vt 0.294662 0.426062 vt 0.300002 0.435825 vt 0.302717 0.434993 vt 0.301383 0.438547 vt 0.304811 0.430533 vt 0.304168 0.433480 vt 0.304743 0.424834 vt 0.304046 0.427006 vt 0.302991 0.420587 vt 0.301626 0.422181 vt 0.300086 0.420480 vt 0.297635 0.422060 vt 0.292911 0.431154 vt 0.619748 0.438411 vt 0.612440 0.446452 vt 0.619863 0.424261 vt 0.626809 0.456702 vt 0.622481 0.469391 vt 0.645673 0.462798 vt 0.648552 0.477037 vt 0.671549 0.470138 vt 0.690897 0.494137 vt 0.666480 0.457838 vt 0.672439 0.443163 vt 0.677603 0.449441 vt 0.669348 0.427997 vt 0.688164 0.425470 vt 0.692690 0.455538 vt 0.643387 0.418507 vt 0.645233 0.418856 vt 0.625203 0.421242 vt 0.452041 0.455473 vt 0.452575 0.455592 vt 0.461005 0.429292 vt 0.458921 0.479080 vt 0.461890 0.481782 vt 0.479208 0.486841 vt 0.487362 0.490366 vt 0.498066 0.478837 vt 0.508938 0.481454 vt 0.503350 0.455149 vt 0.514556 0.455154 vt 0.507400 0.428964 vt 0.526390 0.426086 vt 0.496856 0.431542 vt 0.478133 0.423781 vt 0.485618 0.420380 vt 0.220765 0.455769 vt 0.221085 0.456765 vt 0.222513 0.446077 vt 0.222476 0.465960 vt 0.222942 0.467359 vt 0.226880 0.468110 vt 0.227614 0.469602 vt 0.232193 0.465333 vt 0.231238 0.464001 vt 0.232972 0.455414 vt 0.233966 0.456396 vt 0.232588 0.447550 vt 0.231715 0.446917 vt 0.226990 0.443074 vt 0.227590 0.443558 vt 0.222162 0.445490 vt 0.220441 0.447100 vt 0.220637 0.452443 vt 0.221988 0.443386 vt 0.221479 0.454775 vt 0.221998 0.461421 vt 0.224637 0.456356 vt 0.225795 0.463294 vt 0.229657 0.459663 vt 0.227932 0.453243 vt 0.229444 0.446834 vt 0.231322 0.452131 vt 0.230363 0.444677 vt 0.228743 0.440492 vt 0.225322 0.437579 vt 0.226265 0.441280 vt 0.221683 0.439359 vt 0.219300 0.433988 vt 0.219954 0.440056 vt 0.220988 0.433000 vt 0.220207 0.440708 vt 0.220882 0.447051 vt 0.222684 0.442114 vt 0.223618 0.448498 vt 0.226452 0.445664 vt 0.225197 0.439398 vt 0.226252 0.433754 vt 0.227718 0.439813 vt 0.227068 0.434024 vt 0.225586 0.428168 vt 0.222906 0.425628 vt 0.224105 0.431371 vt 0.220147 0.427209 vt 0.218579 0.431572 vt 0.219456 0.438201 vt 0.219348 0.424886 vt 0.221792 0.439593 vt 0.224146 0.436917 vt 0.225112 0.431342 vt 0.224459 0.425824 vt 0.221929 0.423321 vt 0.300560 0.429721 vt 0.302176 0.434672 vt 0.300368 0.423386 vt 0.304550 0.433877 vt 0.306385 0.429619 vt 0.306332 0.424179 vt 0.304816 0.420125 vt 0.302295 0.420023 vt 0.630493 0.430660 vt 0.627166 0.444600 vt 0.624329 0.439291 vt 0.628994 0.454738 vt 0.628494 0.424592 vt 0.640930 0.463437 vt 0.642619 0.459887 vt 0.653515 0.460331 vt 0.658069 0.460089 vt 0.671010 0.454564 vt 0.673294 0.445198 vt 0.679659 0.445906 vt 0.673126 0.435738 vt 0.678950 0.433502 vt 0.679724 0.436460 vt 0.681594 0.445707 vt 0.677738 0.453639 vt 0.675379 0.457404 vt 0.653010 0.429922 vt 0.640371 0.426511 vt 0.641421 0.421685 vt 0.657350 0.424630 vt 0.676737 0.443356 vt 0.675468 0.432014 vt 0.674168 0.455066 vt 0.670682 0.427490 vt 0.666120 0.431266 vt 0.665078 0.426066 vt 0.663817 0.444830 vt 0.664191 0.458026 vt 0.655524 0.445985 vt 0.668561 0.460696 vt 0.770765 0.468391 vt 0.776691 0.450513 vt 0.774260 0.454219 vt 0.770024 0.432883 vt 0.764614 0.431937 vt 0.765786 0.476647 vt 0.751242 0.426147 vt 0.739181 0.424025 vt 0.718436 0.431356 vt 0.725926 0.450759 vt 0.713302 0.454804 vt 0.733315 0.471458 vt 0.720266 0.478106 vt 0.752638 0.475871 vt 0.741322 0.484998 vt 0.777578 0.448865 vt 0.772025 0.432775 vt 0.772621 0.465216 vt 0.756216 0.426398 vt 0.740068 0.425503 vt 0.746622 0.432670 vt 0.744765 0.432926 vt 0.744091 0.427998 vt 0.744548 0.437945 vt 0.742045 0.433273 vt 0.757328 0.472373 vt 0.741627 0.472138 vt 0.721859 0.444436 vt 0.725168 0.446925 vt 0.726585 0.431545 vt 0.725072 0.456447 vt 0.728133 0.461885 vt 0.731788 0.429580 vt 0.732601 0.459856 vt 0.734626 0.466311 vt 0.740209 0.460624 vt 0.739523 0.455109 vt 0.741109 0.442652 vt 0.741377 0.445243 vt 0.738814 0.430283 vt 0.743987 0.430767 vt 0.730505 0.427231 vt 0.732665 0.425857 vt 0.723508 0.431978 vt 0.725959 0.443870 vt 0.722632 0.444425 vt 0.724133 0.433584 vt 0.728239 0.453242 vt 0.725458 0.454836 vt 0.733837 0.455851 vt 0.732305 0.457734 vt 0.739242 0.452084 vt 0.738827 0.453549 vt 0.740564 0.442325 vt 0.740387 0.442709 vt 0.737414 0.432297 vt 0.738108 0.432953 vt 0.732386 0.430344 vt 0.730476 0.429399 vt 0.735709 0.435254 vt 0.730838 0.439750 vt 0.731851 0.431311 vt 0.737202 0.442046 vt 0.732695 0.447855 vt 0.727197 0.434112 vt 0.740834 0.443937 vt 0.737247 0.450112 vt 0.741631 0.446853 vt 0.744303 0.441206 vt 0.745150 0.434134 vt 0.742707 0.438414 vt 0.740722 0.430308 vt 0.743582 0.427342 vt 0.739897 0.425451 vt 0.736078 0.428052 vt 0.736519 0.428182 vt 0.058770 0.436875 vt 0.055526 0.438739 vt 0.057510 0.426697 vt 0.060306 0.447120 vt 0.057510 0.450781 vt 0.064945 0.450535 vt 0.063522 0.454795 vt 0.069602 0.450781 vt 0.069621 0.447120 vt 0.071186 0.436875 vt 0.071639 0.438739 vt 0.069602 0.426697 vt 0.069621 0.426631 vt 0.064945 0.423216 vt 0.063522 0.422683 vt 0.060306 0.426631 vt 0.063711 0.430932 vt 0.060698 0.437248 vt 0.062030 0.446894 vt 0.062030 0.427603 vt 0.066047 0.450109 vt 0.070088 0.446894 vt 0.071440 0.437248 vt 0.069431 0.430932 vt 0.070088 0.427603 vt 0.066047 0.424388 vt 0.367638 0.430698 vt 0.367024 0.425421 vt 0.366747 0.435757 vt 0.364799 0.423920 vt 0.362507 0.425591 vt 0.361657 0.429830 vt 0.362292 0.434286 vt 0.364473 0.436607 vt 0.460080 0.453644 vt 0.455530 0.469323 vt 0.454708 0.461839 vt 0.459566 0.482294 vt 0.460111 0.441608 vt 0.458432 0.490020 vt 0.473669 0.495593 vt 0.475328 0.489411 vt 0.485564 0.490553 vt 0.490588 0.482966 vt 0.488912 0.473604 vt 0.494970 0.462735 vt 0.484744 0.456197 vt 0.489730 0.442280 vt 0.473504 0.449784 vt 0.475090 0.435163 vt 0.458609 0.431785 vt 0.433344 0.465428 vt 0.432688 0.465075 vt 0.436444 0.461010 vt 0.432809 0.473587 vt 0.431950 0.472167 vt 0.432942 0.470355 vt 0.433482 0.464843 vt 0.436745 0.462379 vt 0.462033 0.502549 vt 0.472427 0.504370 vt 0.473034 0.502903 vt 0.478426 0.499265 vt 0.482198 0.498312 vt 0.460859 0.500748 vt 0.479127 0.490507 vt 0.484360 0.483448 vt 0.476955 0.478040 vt 0.480568 0.468311 vt 0.470332 0.476615 vt 0.471493 0.462901 vt 0.461334 0.475823 vt 0.461340 0.462943 vt 0.126675 0.426939 vt 0.125239 0.437476 vt 0.125905 0.447558 vt 0.591534 0.482441 vt 0.592185 0.479216 vt 0.586654 0.465189 vt 0.585994 0.459124 vt 0.573850 0.460037 vt 0.569889 0.452460 vt 0.561793 0.466536 vt 0.554423 0.459199 vt 0.559099 0.484237 vt 0.549866 0.479317 vt 0.582493 0.456350 vt 0.584017 0.444794 vt 0.587839 0.465438 vt 0.584203 0.451430 vt 0.585630 0.479952 vt 0.581344 0.433641 vt 0.574728 0.430462 vt 0.574899 0.447437 vt 0.568557 0.434852 vt 0.566209 0.452951 vt 0.567218 0.446408 vt 0.564396 0.467465 vt 0.570060 0.457561 vt 0.568400 0.481473 vt 0.576500 0.460740 vt 0.577322 0.485466 vt 0.588361 0.500142 vt 0.580240 0.436794 vt 0.581813 0.437099 vt 0.580687 0.446926 vt 0.578492 0.428581 vt 0.579676 0.427615 vt 0.574416 0.426239 vt 0.574561 0.424911 vt 0.569802 0.428644 vt 0.570633 0.429472 vt 0.569779 0.437984 vt 0.568741 0.438472 vt 0.570944 0.447955 vt 0.571542 0.446197 vt 0.575614 0.448539 vt 0.575998 0.450659 vt 0.579378 0.445306 vt 0.576705 0.444235 vt 0.578656 0.438324 vt 0.576273 0.435464 vt 0.577345 0.431768 vt 0.578024 0.445117 vt 0.574312 0.429899 vt 0.572562 0.435931 vt 0.571501 0.432480 vt 0.570865 0.439273 vt 0.572993 0.444702 vt 0.572179 0.445829 vt 0.575217 0.447698 vt 0.107195 0.428467 vt 0.105366 0.431485 vt 0.107081 0.434182 vt 0.105932 0.435754 vt 0.106105 0.427286 vt 0.107873 0.436828 vt 0.109737 0.434033 vt 0.109877 0.435300 vt 0.110611 0.431768 vt 0.109842 0.428910 vt 0.110028 0.428164 vt 0.108090 0.426425 vt 0.144977 0.440891 vt 0.144073 0.447048 vt 0.142380 0.441368 vt 0.144181 0.447970 vt 0.143884 0.434328 vt 0.145829 0.452297 vt 0.150018 0.452836 vt 0.149196 0.449587 vt 0.153930 0.449572 vt 0.154052 0.446656 vt 0.155008 0.444853 vt 0.154800 0.443415 vt 0.155479 0.439615 vt 0.152971 0.438166 vt 0.153669 0.433013 vt 0.148809 0.437628 vt 0.148741 0.431397 vt 0.147868 0.446460 vt 0.147579 0.450944 vt 0.146008 0.451251 vt 0.147654 0.455576 vt 0.146508 0.445952 vt 0.151897 0.454805 vt 0.151103 0.455721 vt 0.154385 0.452251 vt 0.154198 0.452658 vt 0.154705 0.447767 vt 0.154701 0.447360 vt 0.153219 0.444078 vt 0.153005 0.443034 vt 0.150333 0.443907 vt 0.149543 0.442889 vt 0.148958 0.434293 vt 0.148263 0.443098 vt 0.148521 0.439363 vt 0.149989 0.437180 vt 0.149517 0.446296 vt 0.152135 0.437725 vt 0.151959 0.446648 vt 0.154101 0.444688 vt 0.149047 0.454634 vt 0.154070 0.436241 vt 0.154424 0.433042 vt 0.154400 0.440954 vt 0.153143 0.437755 vt 0.153395 0.430155 vt 0.151210 0.429609 vt 0.150656 0.437404 vt 0.149276 0.431093 vt 0.152498 0.427756 vt 0.152076 0.430714 vt 0.150435 0.431109 vt 0.151260 0.433899 vt 0.150826 0.428081 vt 0.152742 0.433439 vt 0.154420 0.434036 vt 0.153160 0.434510 vt 0.154930 0.433182 vt 0.156450 0.429781 vt 0.155339 0.430153 vt 0.154511 0.427364 vt 0.154093 0.426459 vt 0.152591 0.426752 vt 0.154651 0.427996 vt 0.153355 0.430275 vt 0.154771 0.431692 vt 0.153914 0.432783 vt 0.153743 0.427551 vt 0.155370 0.433333 vt 0.156749 0.431236 vt 0.156773 0.432139 vt 0.156025 0.432738 vt 0.157160 0.429415 vt 0.156625 0.427541 vt 0.156593 0.426907 vt 0.155136 0.426357 vt 0.633927 0.438324 vt 0.631725 0.451970 vt 0.629236 0.449846 vt 0.632335 0.463899 vt 0.630506 0.458728 vt 0.631974 0.435794 vt 0.634694 0.465615 vt 0.642692 0.470164 vt 0.641429 0.468584 vt 0.650532 0.465615 vt 0.650479 0.463899 vt 0.652802 0.451970 vt 0.653116 0.449846 vt 0.649735 0.438324 vt 0.649699 0.435794 vt 0.641645 0.433776 vt 0.640669 0.431109 vt 0.638463 0.441566 vt 0.635288 0.447910 vt 0.636890 0.436583 vt 0.640186 0.450238 vt 0.637653 0.459238 vt 0.644559 0.453129 vt 0.643754 0.463014 vt 0.649674 0.459238 vt 0.648776 0.450238 vt 0.649970 0.441566 vt 0.651371 0.447910 vt 0.649007 0.436583 vt 0.648265 0.432895 vt 0.642798 0.432807 vt 0.639590 0.432895 vt 0.640288 0.436836 vt 0.641639 0.444087 vt 0.645099 0.446504 vt 0.648436 0.444087 vt 0.649379 0.436836 vt 0.471087 0.493023 vt 0.461851 0.491633 vt 0.447801 0.465286 vt 0.444381 0.472057 vt 0.451141 0.472559 vt 0.449228 0.485952 vt 0.453655 0.462770 vt 0.441503 0.488059 vt 0.450132 0.495375 vt 0.452035 0.496994 vt 0.442786 0.490150 vt 0.447565 0.480831 vt 0.453545 0.486977 vt 0.454496 0.473354 vt 0.448818 0.467917 vt 0.449083 0.464568 vt 0.454640 0.464160 vt 0.437892 0.468327 vt 0.435175 0.474659 vt 0.441406 0.480955 vt 0.441070 0.463395 vt 0.435172 0.480076 vt 0.436116 0.482394 vt 0.436816 0.435520 vt 0.436861 0.432263 vt 0.438510 0.434300 vt 0.437823 0.430227 vt 0.439298 0.430404 vt 0.440439 0.432767 vt 0.440224 0.436497 vt 0.439205 0.439253 vt 0.437791 0.438355 vt 0.442428 0.462179 vt 0.442684 0.461415 vt 0.441036 0.475182 vt 0.444769 0.467828 vt 0.445338 0.474973 vt 0.446573 0.463926 vt 0.441215 0.482853 vt 0.445895 0.457617 vt 0.442199 0.452094 vt 0.442553 0.457842 vt 0.436952 0.457157 vt 0.436900 0.460508 vt 0.433972 0.456398 vt 0.433569 0.462304 vt 0.433664 0.464073 vt 0.433170 0.470619 vt 0.436707 0.472132 vt 0.436456 0.479435 vt 0.440517 0.453001 vt 0.442842 0.448171 vt 0.443860 0.457825 vt 0.444781 0.449364 vt 0.440821 0.463931 vt 0.443534 0.441478 vt 0.441234 0.437873 vt 0.441775 0.444806 vt 0.438043 0.437554 vt 0.437584 0.444402 vt 0.436133 0.440721 vt 0.435081 0.448405 vt 0.435934 0.445750 vt 0.434829 0.454763 vt 0.437048 0.451581 vt 0.437820 0.451019 vt 0.437302 0.461424 vt 0.439697 0.442769 vt 0.441268 0.438758 vt 0.441934 0.442075 vt 0.442432 0.436272 vt 0.440090 0.446295 vt 0.441639 0.433294 vt 0.439967 0.430445 vt 0.440548 0.433191 vt 0.437981 0.432969 vt 0.436438 0.432942 vt 0.436464 0.435741 vt 0.436356 0.437038 vt 0.436330 0.440097 vt 0.437753 0.441255 vt 0.437887 0.444625 vt 0.440926 0.437441 vt 0.441253 0.432089 vt 0.439463 0.441382 vt 0.439672 0.429318 vt 0.437575 0.429197 vt 0.437734 0.430299 vt 0.436367 0.431796 vt 0.436302 0.435806 vt 0.437630 0.439919 vt 0.741973 0.429336 vt 0.739252 0.433572 vt 0.742332 0.437152 vt 0.740425 0.439186 vt 0.739886 0.427727 vt 0.743261 0.440749 vt 0.745975 0.436679 vt 0.745954 0.438492 vt 0.746609 0.432646 vt 0.745607 0.428862 vt 0.745393 0.427033 vt 0.742527 0.425470 vt 0.884599 0.496573 vt 0.358264 0.428924 vt 0.532070 0.672062 vt 0.099233 0.755781 vt 0.683775 0.808948 vt 0.104309 0.816718 vt 0.183113 0.461527 vt 0.144731 0.446805 vt 0.145430 0.424001 vt 0.737953 0.430640 vn -0.000300 -0.000400 -1.000000 vn 0.000500 0.254400 -0.967100 vn -0.654500 0.281200 -0.701800 vn 0.705800 0.226900 -0.671100 vn 0.095700 0.595700 -0.797500 vn 0.026500 0.531900 -0.846400 vn -0.069300 0.704700 -0.706200 vn 0.006600 0.005600 1.000000 vn -0.007400 0.158000 0.987400 vn 0.704800 0.213300 0.676500 vn -0.592300 0.249400 0.766200 vn -0.146600 0.141200 0.979100 vn -0.402400 0.345300 0.847900 vn 0.062200 0.565700 0.822300 vn 1.000000 0.000300 0.000000 vn 0.986800 0.161700 0.004400 vn 0.873600 0.472500 0.116200 vn 0.940000 0.339000 -0.037300 vn 0.932600 0.352800 -0.075800 vn -0.999600 0.026500 0.005200 vn -0.918200 0.396000 0.012600 vn -0.756300 0.624600 -0.194600 vn -0.858400 0.507100 -0.077200 vn -0.610100 0.756500 0.235600 vn 0.107700 0.551100 -0.827500 vn -0.033900 -0.057800 -0.997800 vn 0.089700 -0.066700 -0.993700 vn -0.020800 -0.637500 -0.770200 vn -0.263800 -0.551200 -0.791500 vn -0.345400 -0.057300 -0.936700 vn -0.272900 0.453900 -0.848200 vn -0.039400 0.570000 -0.820700 vn 0.138500 0.107500 0.984500 vn 0.928400 0.042800 0.369100 vn 0.441400 0.876400 0.192700 vn 0.544400 -0.791700 0.277300 vn 0.790700 -0.059500 -0.609300 vn -0.204400 0.447900 0.870400 vn 0.181100 0.108900 0.977400 vn -0.066600 0.072400 0.995100 vn -0.085300 -0.098800 0.991400 vn -0.547400 0.026800 0.836400 vn -0.754500 0.633400 -0.172000 vn -0.936200 0.006900 -0.351400 vn -0.990000 -0.009800 0.140700 vn -0.749400 -0.597200 -0.286000 vn -0.738000 -0.674300 -0.026400 vn -0.938100 -0.057800 -0.341600 vn -0.551100 0.811100 -0.195700 vn -0.693000 0.704500 0.153100 vn 0.049900 0.997500 -0.050500 vn -0.001700 0.997100 -0.075700 vn -0.000100 1.000000 0.000100 vn -0.049300 0.997600 -0.048900 vn -0.076500 0.997100 0.000400 vn -0.049000 0.997600 0.049300 vn 0.000400 0.997100 0.076500 vn 0.049300 0.997600 0.048900 vn 0.079100 0.996900 0.004900 vn -0.129200 -0.983100 -0.129800 vn -0.049000 -0.983700 -0.172700 vn -0.009600 -0.999900 -0.009400 vn -0.000300 -0.987100 -0.160100 vn 0.000000 -1.000000 -0.001300 vn -0.001900 -1.000000 0.001400 vn -0.166000 -0.986100 0.001400 vn -0.183600 -0.982200 -0.038800 vn -0.707100 0.000000 -0.707100 vn 0.000000 0.000000 -1.000000 vn -0.004300 -0.008000 -1.000000 vn 0.707100 -0.000000 -0.707100 vn 0.700900 -0.009500 -0.713200 vn -0.708600 0.012700 -0.705500 vn -0.713200 -0.007400 -0.700900 vn 1.000000 0.000000 0.000000 vn 1.000000 0.000100 0.000100 vn 0.707100 0.000300 0.707100 vn 0.720900 0.001000 0.693100 vn -0.000000 0.000000 1.000000 vn 0.000800 0.001300 1.000000 vn -0.707100 0.002600 0.707100 vn -0.693200 0.006200 0.720800 vn -0.700500 0.009300 0.713600 vn -1.000000 0.000000 -0.000000 vn -1.000000 -0.002400 0.004800 vn 0.000100 0.000000 -1.000000 vn -0.003900 -0.007600 -1.000000 vn -0.713600 -0.009400 -0.700500 vn 1.000000 0.000000 0.000100 vn 0.707100 0.000000 0.707200 vn 0.693800 -0.000000 -0.720200 vn -0.707100 0.000000 0.707100 vn -0.700500 0.009400 0.713600 vn -1.000000 0.000000 -0.000100 vn -1.000000 -0.007600 0.003900 vn -0.706900 0.000000 -0.707300 vn 0.000300 0.000000 -1.000000 vn -0.003200 -0.007100 -1.000000 vn 0.707200 -0.000000 -0.707000 vn 1.000000 0.000000 0.000300 vn 0.707000 -0.000000 0.707200 vn -0.000300 0.000000 1.000000 vn -0.000100 -0.000000 1.000000 vn -0.707200 -0.000000 0.707000 vn -1.000000 0.000000 -0.000200 vn -1.000000 -0.007100 0.003200 vn 0.000500 0.993300 0.115500 vn -0.060400 0.996300 0.060800 vn 0.707300 0.000000 -0.706900 vn -0.707000 0.000000 -0.707200 vn -0.115500 0.993300 0.000500 vn -0.060800 0.996300 -0.060400 vn 0.706900 0.000000 0.707300 vn -0.001400 0.993300 -0.115500 vn 0.062900 0.996000 -0.063300 vn -0.707300 0.000000 0.706900 vn 0.115500 0.993300 0.000200 vn 0.060800 0.996300 0.060400 vn -0.044500 -0.996600 -0.069500 vn -0.119300 -0.909800 -0.397500 vn -0.054700 -0.979000 -0.196200 vn -0.331400 -0.682400 0.651600 vn -0.032500 -0.987000 0.157400 vn -0.132100 -0.982700 0.129600 vn -0.098600 -0.992800 0.067800 vn -0.494900 -0.861200 0.115700 vn -0.357600 -0.932800 0.043900 vn 0.523200 -0.852200 0.007400 vn -0.064500 -0.997900 -0.003100 vn -0.186500 -0.980400 0.063700 vn 0.106600 -0.985200 0.133900 vn -0.000100 -0.999100 0.042800 vn 0.030100 -0.986200 0.162700 vn -0.022600 -0.976100 -0.216000 vn -0.034300 -0.867200 -0.496800 vn 0.000300 -0.999300 0.037500 vn -0.000100 -0.985800 0.167700 vn 0.064400 -0.980300 0.186700 vn 0.099700 -0.986400 -0.130400 vn 0.010800 -0.988400 0.151700 vn 0.105200 -0.993000 -0.053700 vn -0.135300 -0.986100 0.096400 vn -0.522700 -0.852200 0.021600 vn 0.078700 -0.996500 0.027400 vn 0.116400 -0.991600 -0.056700 vn -0.990400 0.037300 -0.133200 vn -0.416700 0.045500 -0.907900 vn -0.292700 -0.804600 -0.516600 vn -0.775100 0.630200 -0.044600 vn -0.225500 0.841800 -0.490500 vn 0.472000 0.630100 -0.616600 vn 0.544400 0.037000 -0.838000 vn 0.146100 0.634200 0.759300 vn 0.343800 0.935000 0.087100 vn -0.172000 0.983000 -0.064100 vn 0.021900 0.643900 -0.764800 vn 0.015600 0.653500 -0.756700 vn -0.237600 0.969400 -0.061600 vn -0.140500 0.744700 0.652400 vn 0.008800 -0.999900 -0.009400 vn 0.098200 -0.995200 0.003500 vn 0.001400 -1.000000 0.001300 vn 0.035600 -0.983900 -0.175200 vn -0.062500 -0.800800 0.595600 vn -0.084300 -0.954100 0.287300 vn -0.036300 -0.998700 0.034800 vn -0.050600 -0.982900 0.177100 vn 0.193300 -0.655700 -0.729900 vn 0.326700 -0.056900 -0.943400 vn 0.954500 -0.053800 -0.293400 vn 0.255200 0.578100 -0.775000 vn 0.611300 0.766600 -0.196500 vn 0.624100 0.615500 0.481300 vn 0.800100 0.001600 0.599800 vn 0.589700 -0.619500 0.518100 vn 0.575200 -0.788100 -0.219100 vn -0.285000 -0.907300 0.309200 vn -0.625100 -0.370200 0.687200 vn -0.379800 -0.380700 0.843100 vn -0.620600 0.366100 0.693400 vn -0.382100 0.370700 0.846500 vn -0.186800 -0.349700 0.918100 vn -0.057100 -0.902000 0.427900 vn -0.176900 -0.920500 0.348400 vn -0.009300 -0.999900 0.009300 vn 0.000000 -1.000000 0.002400 vn 0.000400 -1.000000 0.000800 vn -0.288400 -0.955300 -0.064700 vn 0.082900 -0.996500 -0.010500 vn -0.372200 -0.824500 -0.426300 vn 0.214400 -0.976300 0.030200 vn 0.117000 -0.992400 0.037100 vn 0.058400 -0.998200 -0.011400 vn 0.008800 -0.999900 0.009200 vn 0.551300 -0.650400 -0.522600 vn 0.867200 -0.002400 -0.497900 vn 0.834700 -0.051500 -0.548300 vn 0.632300 0.636000 -0.442400 vn 0.774900 -0.070900 -0.628100 vn 0.519900 -0.660600 -0.541600 vn 0.657000 0.676900 -0.331900 vn 0.008900 0.999700 0.021900 vn -0.030400 0.978300 -0.204800 vn -0.692500 0.683900 0.229600 vn -0.744000 0.661700 -0.093400 vn -0.641100 0.541000 -0.544300 vn -0.980400 0.005500 0.196800 vn -0.993600 -0.002900 -0.113000 vn -0.768400 -0.637300 -0.058700 vn -0.711300 -0.646200 -0.276600 vn -0.745700 -0.027600 -0.665700 vn -0.117300 -0.930700 -0.346400 vn -0.069300 -0.939700 -0.334800 vn 0.876700 0.000500 -0.481000 vn 0.821700 -0.000200 -0.569900 vn 0.546000 -0.663400 -0.511600 vn 0.518700 0.639000 -0.568000 vn 0.597000 0.677600 -0.429500 vn -0.163600 0.918400 -0.360100 vn -0.038500 0.996100 -0.080000 vn -0.695800 0.680700 0.229100 vn -0.744900 0.666600 -0.027200 vn -0.944700 -0.004000 0.327900 vn -0.938200 0.001900 0.346000 vn -0.730900 -0.668900 0.135500 vn 0.094500 -0.972700 0.212000 vn -0.088300 -0.975100 -0.203300 vn 0.718700 -0.657300 -0.226700 vn 0.912400 0.001800 -0.409300 vn 0.931500 0.003200 -0.363700 vn 0.794700 -0.597800 0.105200 vn 0.507200 0.628400 -0.589800 vn 0.477800 0.606000 -0.636000 vn -0.200500 0.873000 -0.444700 vn -0.238600 0.813100 -0.531000 vn -0.802000 0.572800 -0.169300 vn -0.803600 0.592800 -0.052300 vn -0.955800 -0.005300 0.294000 vn -0.953500 -0.007700 0.301300 vn -0.519100 -0.627300 0.580500 vn -0.636100 -0.608300 0.474800 vn -0.516200 -0.663100 0.542000 vn 0.156600 -0.924000 0.348800 vn 0.222600 -0.846500 0.483500 vn 0.759700 -0.650200 -0.007000 vn -0.593300 0.558700 0.579500 vn -0.482200 0.010100 0.876000 vn -0.817400 -0.011200 0.576000 vn -0.539500 -0.689600 0.483200 vn -0.988100 -0.036800 -0.149100 vn -0.743100 0.585100 0.324700 vn -0.219800 -0.687500 0.692100 vn -0.052700 -0.992400 0.111600 vn -0.014100 -0.999400 0.031200 vn 0.332100 -0.840700 -0.427700 vn -0.080400 -0.741900 -0.665600 vn 0.411900 -0.078800 -0.907800 vn -0.213200 -0.053000 -0.975600 vn -0.050700 0.758800 -0.649400 vn -0.376000 0.610800 -0.696800 vn -0.480700 0.876900 0.003300 vn -0.547400 0.828200 -0.120000 vn 0.168200 0.855600 0.489500 vn 0.677700 0.029200 0.734700 vn -0.120600 0.035300 0.992100 vn 0.046100 -0.712300 0.700400 vn -0.398100 0.620700 0.675500 vn 0.365700 -0.880800 0.300900 vn -0.124400 -0.992100 -0.013000 vn -0.063400 -0.988400 0.138200 vn -0.068000 -0.997500 -0.016800 vn -0.284200 -0.704800 -0.650000 vn -0.127200 -0.990900 0.044300 vn -0.177300 -0.982300 0.060300 vn 0.144600 -0.670900 0.727300 vn 0.129600 -0.669500 0.731500 vn -0.171000 -0.983600 0.057700 vn -0.301700 -0.713900 -0.631900 vn -0.288100 0.942600 0.168800 vn -0.467600 0.869600 0.158900 vn -0.350500 0.901100 -0.255200 vn 0.972800 -0.006500 0.231600 vn 0.930500 -0.055800 -0.362100 vn 0.728400 -0.683400 0.049800 vn 0.780200 0.618600 -0.092500 vn 0.557400 0.613400 -0.559500 vn 0.005100 0.947100 -0.320800 vn -0.161800 0.870600 -0.464700 vn -0.729000 0.578600 -0.365700 vn -0.625200 0.779900 -0.029300 vn -0.984700 0.016400 0.173400 vn -0.976400 -0.017900 -0.215200 vn -0.751300 -0.659200 -0.029900 vn -0.547100 -0.794500 -0.263600 vn -0.798600 -0.033200 -0.600900 vn -0.063100 -0.996100 0.061500 vn -0.106100 -0.984800 0.137400 vn 0.617900 -0.705300 0.347500 vn 0.888000 -0.439600 -0.134800 vn 0.999900 -0.006900 -0.009400 vn 0.921500 -0.006500 0.388300 vn 0.891200 0.447500 -0.074200 vn 0.798100 0.596800 0.082700 vn 0.741900 -0.652000 0.156700 vn 0.691800 0.621600 -0.367500 vn 0.270000 0.930900 -0.245900 vn -0.054700 0.980300 -0.189900 vn -0.921100 -0.010500 -0.389200 vn -0.641500 -0.022000 -0.766800 vn -0.534600 0.689500 -0.488600 vn -0.595100 -0.675700 -0.435000 vn -0.439200 -0.699600 -0.563700 vn -0.106500 0.006300 -0.994300 vn 0.690400 -0.597800 -0.407500 vn 0.250500 -0.941900 -0.223900 vn 0.011200 -0.978700 -0.205200 vn 0.418100 0.027300 0.908000 vn 0.408400 0.687800 0.600100 vn 0.340200 0.038300 0.939600 vn 0.310700 -0.680900 0.663200 vn 0.212400 0.974300 -0.074600 vn -0.001100 0.652400 -0.757900 vn -0.048900 0.656700 -0.752600 vn 0.218600 0.973200 -0.072100 vn 0.371100 0.679300 0.633200 vn -0.163400 -0.061300 -0.984700 vn -0.104000 -0.708300 -0.698200 vn -0.256800 -0.045800 -0.965400 vn 0.063700 -0.997900 -0.010800 vn -0.005300 0.032600 0.999500 vn -0.700900 0.018400 0.713000 vn -0.457200 0.780700 0.426000 vn -0.433400 -0.774700 0.460400 vn -0.599200 -0.794000 0.102600 vn 0.074500 -0.996000 -0.049100 vn 0.035800 -0.997400 -0.063200 vn 0.539700 -0.722900 -0.431300 vn 0.540000 -0.809200 -0.231600 vn 0.971900 -0.004900 -0.235500 vn 0.809300 -0.037000 -0.586200 vn 0.523800 0.755300 -0.393800 vn 0.774500 -0.027500 -0.632000 vn -0.040000 0.998400 0.040100 vn -0.061000 0.998100 0.003300 vn -0.745700 0.018100 0.666000 vn -0.648000 0.035500 0.760800 vn -0.368700 0.733200 0.571400 vn -0.636800 -0.707500 0.306400 vn -0.455300 -0.686300 0.567200 vn -0.113700 -0.550800 0.826800 vn -0.105500 0.039800 0.993600 vn -0.086600 0.612500 0.785700 vn -0.092300 -0.971900 -0.216600 vn -0.091700 -0.992200 -0.084300 vn -0.026900 -0.972400 -0.231700 vn -0.928900 0.024500 0.369400 vn -0.709400 -0.030700 -0.704100 vn -0.421400 -0.883700 -0.203700 vn -0.316800 0.896900 -0.308600 vn 0.344500 -0.049600 -0.937500 vn -0.785900 0.611600 -0.090500 vn -0.233100 0.966800 -0.104700 vn -0.084300 0.995100 -0.051900 vn 0.530100 0.847200 -0.034200 vn -0.231400 0.956200 0.179000 vn 0.305100 -0.035900 -0.951600 vn 0.538400 -0.036100 -0.841900 vn 0.525600 0.656200 -0.541500 vn 0.559300 -0.607500 -0.564100 vn 0.602100 -0.595900 -0.531400 vn 0.997800 -0.006900 -0.066500 vn 0.615400 -0.773500 0.151300 vn 0.373200 -0.925500 0.064500 vn 0.379200 -0.574900 0.725100 vn 0.409900 -0.483700 0.773300 vn 0.021100 0.101600 0.994600 vn 0.166900 0.092200 0.981600 vn 0.262800 0.656500 0.707100 vn 0.735500 0.082700 0.672400 vn -0.255600 0.673600 0.693500 vn 0.319500 0.945200 0.067800 vn 0.598300 0.550200 0.582500 vn 0.531200 0.669300 0.519400 vn 0.971300 0.171900 0.164600 vn 0.976400 0.196500 -0.089400 vn 0.717400 -0.386300 -0.579800 vn 0.671500 -0.427700 -0.605200 vn 0.942800 0.252400 -0.217800 vn 0.736400 0.519200 0.433700 vn 0.823200 -0.514800 -0.239300 vn 0.138000 -0.882200 -0.450300 vn -0.043500 -0.631400 -0.774300 vn -0.583500 -0.673400 -0.453900 vn -0.686600 -0.366900 -0.627600 vn -0.761200 -0.566800 -0.315100 vn -0.215000 -0.799700 -0.560600 vn -0.974400 -0.161100 -0.157100 vn -0.988300 -0.151900 -0.012500 vn -0.852700 0.383300 0.355000 vn -0.593200 0.229200 0.771800 vn -0.952400 -0.213300 0.217900 vn -0.275200 0.713100 0.644800 vn 0.103100 0.521200 0.847200 vn 0.171500 0.724800 0.667300 vn -0.559100 0.263000 0.786300 vn 0.534500 0.741100 0.406300 vn 0.930200 0.067400 0.360800 vn 0.937100 0.087000 0.338200 vn 0.760200 -0.635700 0.133700 vn 0.521100 0.722900 0.453700 vn 0.725100 -0.666300 0.174200 vn 0.016700 -0.996700 -0.079300 vn 0.073000 -0.990700 -0.114600 vn -0.637000 -0.746500 -0.192200 vn -0.601000 -0.773700 -0.200600 vn -0.984200 -0.096700 -0.148300 vn -0.979900 -0.145300 -0.136600 vn -0.835700 0.543100 0.081200 vn -0.793100 0.607300 0.045300 vn -0.221500 0.943200 0.247500 vn -0.281400 0.906400 0.314900 vn 0.341000 0.022700 0.939800 vn 0.135100 -0.683200 0.717700 vn 0.185100 0.723300 0.665300 vn -0.127700 -0.983700 0.126400 vn -0.272900 -0.943300 -0.189100 vn -0.196400 -0.833500 -0.516400 vn -0.707200 -0.066000 0.703900 vn -0.625200 -0.712100 -0.319500 vn -0.377500 -0.915000 -0.142200 vn -0.378000 0.924500 -0.048600 vn -0.617600 0.770400 -0.158300 vn -0.064200 0.116900 -0.991100 vn -0.034200 0.986900 0.157900 vn 0.034800 0.994800 0.096000 vn -0.009200 0.840100 -0.542400 vn 0.680400 -0.716800 -0.152400 vn 0.973700 -0.026500 -0.226400 vn 1.000000 -0.000500 -0.005900 vn 0.670800 0.725200 0.155600 vn 0.718800 -0.680500 -0.142100 vn 0.682500 0.695100 -0.226000 vn -0.110700 0.983900 -0.140300 vn -0.094600 0.963600 0.250100 vn -0.760500 0.635100 0.135500 vn -0.994500 0.013300 0.103900 vn -0.999500 0.002200 -0.032200 vn -0.746800 -0.659700 0.084300 vn -0.717900 -0.662100 -0.214900 vn -0.081400 -0.996400 -0.022500 vn -0.027300 -0.979400 -0.199900 vn 0.760500 -0.039000 -0.648200 vn 0.859200 -0.035400 -0.510400 vn 0.635000 -0.731200 -0.249300 vn 0.409600 0.653500 -0.636500 vn 0.499200 0.619000 -0.606300 vn -0.258200 0.922200 -0.288000 vn -0.215200 0.879400 -0.424700 vn -0.778100 0.614700 -0.129100 vn -0.687400 0.724000 0.056900 vn -0.775800 0.623800 0.095000 vn -0.916400 0.020600 0.399800 vn -0.983200 0.017800 0.181900 vn -0.699200 -0.652700 0.291600 vn -0.583300 -0.672600 0.455400 vn -0.039100 -0.991400 0.124600 vn -0.045800 -0.992000 0.117600 vn 0.565700 -0.736800 -0.370400 vn -0.820000 -0.013900 -0.572200 vn -0.776500 -0.031200 -0.629300 vn -0.679800 -0.662400 -0.314900 vn -0.456700 0.650000 -0.607400 vn -0.622300 0.622800 -0.474100 vn 0.185000 0.952800 -0.240800 vn -0.147100 0.986900 0.066100 vn 0.633300 0.688100 0.354300 vn 0.239400 0.698700 0.674200 vn 0.565400 0.137800 0.813200 vn 0.453400 0.130900 0.881600 vn 0.125100 -0.401300 0.907400 vn 0.175000 -0.595000 0.784400 vn -0.441300 -0.754600 0.485500 vn -0.239000 -0.948300 0.208800 vn -0.783800 -0.606300 -0.134200 vn -0.795800 -0.605100 0.024500 vn -0.962200 0.004100 -0.272200 vn -0.929900 0.034400 -0.366100 vn -0.526000 0.666200 -0.528700 vn -0.891200 -0.432300 0.137500 vn -0.622800 0.723200 -0.298500 vn 0.002200 0.999900 0.015200 vn 0.179700 0.934700 -0.306800 vn 0.611600 0.688900 0.389000 vn 0.710000 0.659000 0.248300 vn 0.779300 0.045800 0.625000 vn 0.654900 0.104500 0.748500 vn 0.432200 -0.558300 0.708200 vn 0.119400 -0.307100 0.944100 vn -0.252500 -0.865100 0.433400 vn -0.478900 -0.562000 0.674400 vn -0.177800 0.690400 0.701200 vn -0.131500 0.044500 0.990300 vn 0.076900 0.043900 0.996100 vn -0.126300 -0.630200 0.766100 vn 0.154200 -0.630400 0.760800 vn 0.543500 -0.510900 0.666000 vn 0.642700 0.042600 0.764900 vn 0.539100 0.583600 0.607200 vn 0.143500 0.725100 0.673500 vn -0.292400 -0.949200 -0.115900 vn -0.088900 -0.996000 -0.013200 vn -0.555600 -0.699300 -0.449800 vn -0.664700 -0.010800 -0.747100 vn -0.564500 0.734800 -0.376100 vn -0.284400 0.951700 0.115800 vn -0.110000 0.987100 0.116600 vn 0.930200 0.011200 0.366800 vn 0.652300 -0.673300 0.348200 vn 0.646800 0.701400 0.299500 vn 0.093500 -0.984300 0.149400 vn -0.127800 -0.990700 0.046500 vn -0.399900 -0.868900 -0.291800 vn -0.551300 -0.607300 0.572100 vn -0.752600 -0.023300 0.658100 vn -0.983000 -0.061500 -0.172800 vn -0.600600 0.561900 0.568800 vn -0.685100 0.723000 -0.089300 vn -0.393700 0.567100 -0.723500 vn -0.462900 -0.020400 -0.886200 vn -0.337100 -0.611400 -0.715900 vn -0.629000 -0.775600 -0.053200 vn 0.070700 0.992000 0.105000 vn -0.213500 0.976900 0.008500 vn -0.822900 0.043700 -0.566500 vn -0.951500 -0.038800 -0.305200 vn -0.650800 -0.710700 -0.267200 vn -0.751400 0.655800 -0.073000 vn -0.146800 0.928200 0.341800 vn -0.121100 0.949200 0.290400 vn 0.536100 0.671000 0.512200 vn 0.960300 0.119600 0.252000 vn 0.855500 0.020700 0.517400 vn 0.655400 -0.689200 0.309100 vn 0.081100 -0.978800 -0.188100 vn 0.003700 -0.999800 -0.017800 vn 0.103400 -0.825500 0.554800 vn 0.349600 -0.289700 0.891000 vn -0.487400 -0.002900 0.873200 vn 0.518000 0.356500 0.777600 vn -0.008200 0.737400 0.675400 vn -0.519200 0.827800 0.212500 vn -0.942400 0.300500 0.147200 vn -0.933600 -0.358200 -0.014000 vn -0.499500 -0.729000 0.468100 vn -0.221800 0.842700 0.490700 vn -0.161100 0.868100 0.469500 vn -0.294100 0.872900 0.389300 vn 0.435400 0.807200 0.398600 vn -0.585700 0.802900 -0.110500 vn -0.713100 0.352600 0.605900 vn -0.779800 -0.141900 0.609800 vn -0.259300 -0.141000 0.955400 vn -0.598300 -0.673900 0.433600 vn -0.138400 -0.757500 0.638000 vn 0.371600 -0.567800 0.734500 vn 0.399900 -0.055100 0.914900 vn 0.210800 0.414500 0.885300 vn -0.268200 0.478600 0.836100 vn 0.060800 -0.989000 -0.134800 vn 0.174100 -0.970000 -0.169800 vn 0.012500 -0.971600 -0.236100 vn -0.409200 -0.755600 -0.511500 vn 0.689000 -0.723800 -0.036700 vn 0.026200 0.292000 -0.956100 vn -0.406700 0.225200 -0.885400 vn -0.184700 -0.524300 -0.831300 vn -0.340200 0.819200 -0.461700 vn -0.187000 0.872100 -0.452200 vn -0.434800 0.860000 0.267000 vn -0.200900 0.891700 0.405700 vn -0.192500 0.381400 0.904100 vn -0.186400 0.396700 0.898800 vn -0.473500 0.370600 0.799000 vn -0.237300 -0.347700 0.907100 vn -0.094200 -0.316300 0.944000 vn 0.051900 -0.872900 0.485100 vn 0.026300 -0.879200 0.475700 vn 0.246400 -0.944800 -0.216000 vn 0.071200 -0.954100 -0.291000 vn 0.206700 -0.462300 -0.862300 vn 0.961000 0.260800 -0.091500 vn 0.587200 0.808400 0.040600 vn 0.817100 -0.482600 -0.315300 vn 0.104500 -0.937200 -0.332800 vn -0.542300 -0.835200 0.090800 vn -0.850600 -0.312600 0.422700 vn -0.734800 0.364800 0.571900 vn -0.196500 0.889500 0.412500 vn 0.656100 0.737500 0.160000 vn -0.068400 0.403800 -0.912300 vn 0.098900 0.358000 -0.928500 vn 0.448000 -0.337400 -0.827900 vn -0.587700 0.706900 -0.393600 vn -0.345300 0.779200 -0.523100 vn -0.797300 0.498500 0.340300 vn -0.656100 0.721500 0.221100 vn -0.674900 0.196600 0.711300 vn -0.648200 0.009800 0.761400 vn -0.071000 -0.478000 0.875500 vn -0.319800 -0.398100 0.859800 vn 0.182500 -0.851800 0.491100 vn 0.535700 -0.720200 0.440800 vn 0.703100 -0.655800 -0.274800 vn 0.498700 -0.854800 -0.143300 vn -0.651600 0.332600 -0.681800 vn -0.371900 0.392800 -0.841100 vn 0.151600 -0.139700 -0.978500 vn -0.660900 0.750500 0.002100 vn -0.720200 0.679100 -0.142300 vn 0.411500 -0.089700 -0.907000 vn -0.287200 0.651500 0.702200 vn -0.649900 0.466700 0.599900 vn -0.237300 0.002600 0.971400 vn 0.286900 0.154000 0.945500 vn 0.581400 -0.373200 0.723000 vn 0.381500 -0.448500 0.808300 vn 0.755400 -0.624100 0.200000 vn 0.651100 -0.757700 -0.044600 vn 0.262400 -0.798900 -0.541200 vn 0.614900 -0.589200 -0.524100 vn -0.098000 -0.662100 -0.743000 vn 0.033600 0.024000 -0.999100 vn -0.291300 -0.076500 -0.953600 vn -0.245700 0.653800 -0.715700 vn -0.128200 -0.699900 -0.702600 vn 0.018100 0.686100 -0.727200 vn 0.006300 0.997500 0.070800 vn -0.173100 0.984300 -0.033300 vn -0.177600 0.668400 0.722300 vn -0.004700 0.643100 0.765800 vn -0.093800 0.034500 0.995000 vn -0.118200 0.033100 0.992400 vn -0.220700 -0.640000 0.736000 vn -0.159300 -0.628400 0.761400 vn -0.240800 -0.970300 0.021400 vn -0.148800 -0.988900 0.001600 vn 0.168400 0.034900 -0.985100 vn 0.068700 0.060000 -0.995800 vn -0.146000 -0.652000 -0.744000 vn 0.366500 0.680700 -0.634300 vn 0.208400 0.723700 -0.657900 vn 0.358300 0.917800 0.171000 vn 0.218500 0.964800 0.146600 vn 0.101900 0.632700 0.767600 vn 0.181900 0.594200 0.783400 vn -0.076500 0.000200 0.997100 vn -0.088000 0.002700 0.996100 vn -0.268600 -0.651000 0.709900 vn -0.303100 -0.952900 0.012300 vn -0.306300 -0.951800 0.015400 vn -0.098100 -0.671800 -0.734200 vn 0.051700 -0.696100 -0.716100 vn 0.268400 0.010900 -0.963200 vn 0.261500 -0.003600 -0.965200 vn 0.455900 0.646200 -0.612000 vn -0.017900 -0.696300 -0.717600 vn 0.409500 0.683600 -0.604200 vn 0.328900 0.929100 0.169400 vn 0.407900 0.894700 0.182100 vn 0.120800 0.625800 0.770600 vn 0.191300 0.595600 0.780200 vn -0.092600 0.029700 0.995300 vn -0.061500 0.009200 0.998100 vn -0.220600 -0.651000 0.726300 vn -0.254100 -0.654700 0.711900 vn -0.176100 -0.983500 0.041100 vn -0.255600 -0.966500 0.023200 vn -0.263100 -0.658300 0.705300 vn -0.736900 -0.446400 -0.507700 vn -0.831700 0.334900 -0.442900 vn -0.803000 0.263800 -0.534500 vn -0.549200 0.835300 0.024200 vn -0.601300 -0.477400 -0.640800 vn -0.441300 0.896400 -0.040500 vn 0.247500 0.880800 0.403600 vn 0.042900 0.816100 0.576400 vn 0.658800 0.386800 0.645300 vn 0.480300 0.338200 0.809300 vn 0.723500 -0.356600 0.591100 vn 0.654500 -0.367200 0.660900 vn 0.355800 -0.907800 0.221900 vn 0.404400 -0.895200 0.187500 vn -0.201000 -0.955800 -0.214400 vn -0.092700 -0.943100 -0.319400 vn -0.290200 -0.365500 -0.884400 vn 0.293300 0.032200 0.955500 vn 0.579500 0.039800 0.814000 vn 0.239900 -0.736200 0.632900 vn 0.394500 0.670000 0.628800 vn 0.766100 0.438800 0.469700 vn 0.372100 0.926300 0.059000 vn 0.773500 0.633600 -0.014700 vn 0.618600 0.525800 -0.583900 vn 0.671300 0.519500 -0.528700 vn -0.114000 0.803500 -0.584300 vn -0.526400 -0.031800 -0.849700 vn 0.292200 -0.025300 -0.956000 vn 0.085800 -0.757700 -0.646900 vn 0.463800 -0.817600 -0.341000 vn 0.804900 -0.033900 -0.592500 vn 0.021500 -0.997200 -0.071500 vn -0.129800 -0.988500 -0.077400 vn 0.124300 -0.764700 0.632300 vn -0.949300 0.009200 0.314200 vn -0.923200 0.015700 0.384100 vn -0.661400 -0.702200 0.263400 vn -0.704800 0.686800 0.177800 vn -0.650800 0.627200 0.427800 vn -0.001800 0.999900 0.010000 vn 0.058900 0.903800 0.423800 vn 0.720500 0.691900 -0.046800 vn 0.716200 0.600000 0.356500 vn 0.999700 0.020100 -0.010800 vn 0.972200 0.024200 0.233000 vn 0.701000 -0.681000 0.212000 vn 0.499800 -0.770400 0.395800 vn 0.721300 -0.687400 0.084400 vn 0.076100 -0.952700 0.294200 vn -0.004200 -0.999100 0.041800 vn 0.076100 0.019200 -0.996900 vn 0.177600 0.022200 -0.983900 vn 0.083200 -0.680000 -0.728500 vn -0.167500 0.670700 -0.722500 vn 0.135500 0.708300 -0.692800 vn -0.372200 0.927800 0.025900 vn -0.007500 0.994300 0.106300 vn -0.134700 0.658300 0.740600 vn -0.410800 0.647200 0.642200 vn -0.293600 0.032600 0.955400 vn -0.165400 0.040500 0.985400 vn -0.149400 -0.652300 0.743100 vn -0.089800 -0.633200 0.768800 vn 0.179000 -0.978200 0.105500 vn -0.019200 -0.997300 0.070400 vn 0.176500 -0.686900 -0.705000 vn 0.078600 -0.005500 -0.996900 vn 0.047900 0.002400 -0.998800 vn 0.303200 -0.676400 -0.671200 vn -0.326000 0.576500 -0.749200 vn -0.287100 0.615900 -0.733600 vn -0.590000 0.807200 -0.015300 vn -0.517900 0.855400 -0.004700 vn -0.518700 0.579900 0.628300 vn -0.555100 0.549000 0.624900 vn -0.293900 0.006300 0.955800 vn -0.313400 0.018200 0.949500 vn 0.032300 -0.635300 0.771600 vn 0.120200 -0.599100 0.791600 vn 0.423400 -0.893400 0.150100 vn 0.330100 -0.935200 0.128600 vn 0.390800 -0.645800 -0.655900 vn 0.144900 0.019600 -0.989300 vn 0.113500 0.008000 -0.993500 vn 0.423400 -0.633600 -0.647500 vn -0.160000 0.659800 -0.734200 vn -0.305300 0.588600 -0.748500 vn -0.401500 0.915500 0.024800 vn -0.580800 0.813900 -0.013500 vn -0.533700 0.556900 0.636400 vn -0.364400 0.611600 0.702200 vn -0.227400 0.034700 0.973200 vn -0.254800 0.002000 0.967000 vn 0.136600 -0.597000 0.790500 vn 0.103400 -0.631900 0.768100 vn 0.294800 -0.947200 0.125800 vn 0.423800 -0.893800 0.146300 vn 0.359000 -0.672000 -0.647700 vn 0.045600 0.007100 -0.998900 vn -0.157800 0.673300 -0.722300 vn 0.053300 -0.708700 -0.703500 vn -0.310400 0.949700 0.040700 vn -0.378800 0.632800 0.675300 vn -0.334800 0.008300 0.942200 vn -0.220500 -0.651900 0.725500 vn -0.037700 -0.997600 0.058700 vn -0.867000 0.355800 -0.348800 vn -0.415700 0.909500 -0.007400 vn -0.844600 -0.424000 -0.327100 vn 0.289600 0.883200 0.368800 vn 0.689500 0.371600 0.621700 vn 0.702600 -0.375000 0.604800 vn 0.284500 -0.898100 0.335400 vn -0.319700 -0.946200 -0.050500 vn 0.341500 -0.676100 0.652900 vn 0.218300 0.029900 0.975400 vn 0.209200 0.011100 0.977800 vn 0.093000 0.717000 0.690800 vn 0.245500 -0.744100 0.621300 vn -0.188700 0.976600 0.102700 vn 0.004700 0.989500 0.144200 vn 0.080800 0.889000 -0.450700 vn -0.019600 0.998300 -0.055700 vn 0.437300 0.601100 -0.668900 vn 0.533500 0.192400 -0.823700 vn -0.006100 0.182300 -0.983200 vn 0.484100 -0.295800 -0.823500 vn -0.012100 -0.478400 -0.878000 vn -0.580000 -0.396300 -0.711700 vn -0.654900 0.123200 -0.745600 vn -0.558400 0.593500 -0.579700 vn -0.061200 0.735300 -0.675000 vn 0.411100 -0.712800 -0.568200 vn 0.280700 -0.959000 -0.040100 vn 0.171400 -0.985100 -0.013100 vn 0.131900 -0.962300 -0.237800 vn -0.874200 0.016800 -0.485400 vn -0.620300 -0.608400 -0.495100 vn -0.662500 0.628400 -0.407700 vn 0.031200 -0.903700 -0.427100 vn 0.584400 -0.483500 -0.651600 vn -0.207000 -0.833200 -0.512700 vn 0.729200 0.174100 -0.661800 vn 0.459300 0.734200 -0.500000 vn 0.408400 0.115800 -0.905400 vn -0.038800 0.975000 -0.218900 vn -0.661500 0.681000 -0.314200 vn -0.966200 -0.016000 -0.257300 vn -0.926000 -0.034000 -0.375900 vn -0.678400 -0.715100 -0.168700 vn -0.648300 -0.702900 -0.292700 vn -0.834200 -0.039300 -0.550100 vn -0.600400 0.598500 -0.530400 vn -0.099100 -0.995000 -0.013500 vn -0.002500 -0.997400 -0.071600 vn 0.683300 -0.726600 -0.071900 vn 0.892900 0.013700 0.450000 vn 0.996600 -0.005300 -0.082000 vn 0.577500 0.800400 0.160800 vn 0.713500 0.638700 -0.288000 vn -0.056500 0.972000 -0.228200 vn 0.043200 0.878600 -0.475500 vn -0.847500 -0.048700 -0.528600 vn -0.543400 -0.704900 -0.455900 vn -0.579000 0.598200 -0.554000 vn -0.130900 -0.957000 -0.258700 vn 0.052300 -0.993400 -0.102200 vn -0.687100 -0.077400 -0.722400 vn 0.274700 0.012800 -0.961500 vn 0.120400 -0.867700 -0.482300 vn 0.072700 0.880800 -0.467900 vn 0.983500 0.091000 -0.156400 vn -0.102100 0.917800 -0.383700 vn 0.214800 0.953600 -0.210800 vn 0.693100 0.094300 0.714700 vn 0.637900 0.052100 0.768300 vn 0.465900 -0.575600 0.672000 vn 0.574400 0.720200 0.389100 vn 0.633000 0.631400 0.447800 vn 0.511400 -0.791800 0.334000 vn 0.209900 0.948500 -0.237500 vn 0.414300 0.891200 -0.184800 vn -0.114800 0.545900 -0.829900 vn -0.526100 0.538300 -0.658400 vn -0.755900 -0.067500 -0.651200 vn -0.547000 -0.054900 -0.835300 vn -0.375500 -0.691500 -0.617200 vn -0.030000 -0.687200 -0.725900 vn 0.123100 -0.990400 -0.062800 vn 0.049200 -0.998300 0.032100 vn 0.608800 -0.596900 0.522500 vn 0.972400 0.100800 0.210400 vn 0.869200 0.100200 0.484100 vn 0.701500 -0.635400 0.322700 vn 0.668500 0.743700 -0.006100 vn 0.602000 0.756900 0.254400 vn 0.024400 0.961000 -0.275300 vn 0.029200 0.983800 -0.176800 vn -0.630900 0.621600 -0.464300 vn -0.561400 0.623700 -0.544000 vn -0.902200 -0.099900 -0.419700 vn -0.790800 -0.099400 -0.603900 vn -0.490100 -0.743300 -0.455300 vn -0.619700 -0.759200 -0.199100 vn 0.053000 -0.996500 0.064000 vn 0.137400 -0.987500 -0.076700 vn 0.987700 0.100600 0.119600 vn 0.992900 0.099400 0.064900 vn 0.734900 -0.646700 0.204100 vn 0.669400 0.738300 -0.082200 vn 0.676600 0.715400 -0.174400 vn 0.767500 -0.582300 0.268200 vn 0.007700 0.958600 -0.284800 vn 0.034500 0.927900 -0.371100 vn -0.656400 0.614000 -0.438400 vn -0.705100 0.609700 -0.361900 vn -0.941000 -0.101200 -0.323000 vn -0.955000 -0.113900 -0.273900 vn -0.654800 -0.754900 -0.036300 vn -0.638100 -0.761500 -0.113900 vn 0.043400 -0.994600 0.094000 vn 0.026700 -0.985900 0.165000 vn 0.719600 -0.659400 0.217700 vn 0.881500 -0.000300 -0.472100 vn 0.875700 -0.001000 -0.482900 vn 0.680300 -0.684200 -0.262600 vn 0.623500 0.681100 -0.383900 vn 0.564700 0.663900 -0.490300 vn -0.040700 0.995200 -0.088600 vn -0.113800 0.961700 -0.249400 vn -0.745600 0.660700 0.086700 vn -0.695000 0.677600 0.240600 vn -0.932400 -0.000300 0.361300 vn -0.949900 -0.029000 0.311200 vn -0.657500 -0.683400 0.317300 vn -0.673500 -0.686100 0.275100 vn -0.015800 -0.999300 -0.033700 vn 0.034000 -0.996800 0.072100 vn 0.633500 -0.695200 -0.339800 vn 0.482600 -0.608800 -0.629700 vn 0.849300 -0.008600 -0.527900 vn 0.604300 0.679300 -0.416300 vn 0.575400 -0.679000 -0.455900 vn -0.043300 0.995000 -0.089600 vn -0.709800 0.679000 0.187300 vn -0.954200 -0.010200 0.299000 vn -0.796600 -0.604300 -0.012000 vn -0.727600 -0.672500 0.135300 vn -0.070800 -0.984200 -0.162000 vn 0.849600 0.027000 0.526800 vn 0.618800 -0.674500 0.402500 vn 0.467800 0.628000 0.621900 vn -0.018000 -0.997400 0.069900 vn -0.656200 -0.754600 -0.009500 vn -0.994700 -0.098400 0.028500 vn -0.802300 0.535200 0.264200 vn -0.244400 0.859500 0.449000 vn -0.465200 -0.774100 0.429300 vn -0.988100 -0.153600 0.001100 vn -0.931600 -0.025200 0.362600 vn -0.767300 0.639800 0.043500 vn -0.538200 -0.654600 0.530900 vn -0.700900 0.637500 -0.319900 vn -0.080400 0.925600 -0.370000 vn -0.087800 0.964800 -0.247900 vn 0.611600 0.678200 -0.407400 vn 0.650400 0.697800 -0.300100 vn 0.983200 0.069800 -0.168700 vn 0.990300 0.052000 -0.128800 vn 0.825100 -0.520400 0.220000 vn 0.800500 -0.568300 0.190200 vn 0.254100 -0.820200 0.512500 vn 0.165600 -0.863100 0.477100 vn -0.618800 -0.684200 0.386000 vn -0.878500 -0.473900 0.061100 vn -0.831600 -0.550000 0.077300 vn -0.271400 -0.959700 -0.072700 vn -0.949000 0.219400 0.226600 vn -0.983900 0.178400 0.013900 vn -0.942700 0.048900 -0.330000 vn -0.740500 -0.551700 -0.383800 vn -0.278500 -0.911600 -0.302300 vn -0.053300 0.897200 0.438300 vn 0.334500 0.892800 0.301900 vn 0.044000 0.964200 -0.261600 vn 0.702800 0.697900 0.137800 vn 0.668200 0.703700 -0.241300 vn -0.354600 0.919300 -0.170800 vn 0.866300 0.221200 0.447800 vn 0.997000 0.073500 -0.024600 vn 0.757800 -0.244500 0.605000 vn 0.819900 -0.420000 0.389000 vn 0.483600 -0.299100 0.822600 vn 0.359400 -0.706100 0.610100 vn 0.287200 -0.335200 0.897300 vn 0.075200 -0.808600 0.583500 vn 0.548400 -0.663500 -0.509000 vn 0.871100 -0.040200 -0.489500 vn 0.524700 0.690000 -0.498600 vn 0.319300 -0.081400 -0.944100 vn 0.257800 -0.059600 -0.964300 vn 0.145900 -0.707700 -0.691300 vn 0.517400 -0.657900 -0.547300 vn -0.027200 -0.999400 0.019300 vn 0.587600 -0.785800 0.192800 vn -0.078800 -0.630700 0.772000 vn 0.413600 -0.469300 0.780200 vn -0.019900 0.025600 0.999500 vn 0.092100 0.105000 0.990200 vn 0.733300 0.454100 -0.506100 vn 0.429700 -0.123600 -0.894500 vn 0.395400 -0.104200 -0.912600 vn -0.132400 -0.632600 -0.763100 vn 0.749700 0.430900 -0.502200 vn -0.100800 -0.677900 -0.728200 vn -0.440800 -0.891200 -0.107000 vn -0.544100 -0.826400 -0.144700 vn -0.499600 -0.577900 0.645300 vn -0.534000 -0.565300 0.628700 vn -0.183700 0.049600 0.981700 vn -0.131700 0.024300 0.991000 vn 0.284800 0.549700 0.785400 vn 0.354700 0.496300 0.792400 vn 0.653800 0.730400 0.197900 vn 0.702500 0.677400 0.218300 vn 0.376200 0.678600 -0.630800 vn 0.481700 -0.104500 -0.870100 vn 0.453700 -0.116400 -0.883500 vn 0.565200 0.558700 -0.606900 vn 0.378600 -0.739000 -0.557300 vn 0.203800 -0.743600 -0.636800 vn 0.116800 -0.986700 0.113400 vn -0.119200 -0.992700 0.021100 vn -0.298200 -0.626400 0.720200 vn -0.120900 -0.626400 0.770100 vn -0.251200 0.097300 0.963000 vn -0.264600 0.060100 0.962500 vn 0.039600 0.688400 0.724300 vn -0.123600 0.736500 0.665100 vn 0.121600 0.992400 -0.020100 vn 0.368400 0.925900 0.084100 vn 0.412300 0.602800 -0.683100 vn 0.545400 0.562600 -0.621300 vn 0.558000 -0.104300 -0.823300 vn 0.610400 -0.643800 -0.461500 vn 0.515500 -0.706100 -0.485600 vn 0.439800 0.608500 -0.660500 vn 0.269700 -0.946700 0.176400 vn 0.116100 -0.569100 0.814000 vn 0.000300 -0.590100 0.807300 vn -0.180000 0.092100 0.979300 vn -0.013300 0.713200 0.700800 vn -0.105600 0.737400 0.667200 vn 0.132600 0.991000 -0.018100 vn 0.311200 -0.689200 -0.654300 vn 0.641200 -0.056400 -0.765300 vn 0.332400 0.642600 -0.690300 vn 0.362100 0.677200 -0.640500 vn 0.410600 -0.744900 -0.525800 vn -0.192200 0.971300 -0.140200 vn -0.613800 0.716200 0.332200 vn -0.664300 0.680300 0.309700 vn -0.835600 0.016600 0.549100 vn -0.652700 -0.629000 0.422300 vn -0.623000 -0.652500 0.431500 vn -0.126300 -0.992000 0.006600 vn 0.056400 -0.561100 -0.825900 vn 0.403200 0.172100 -0.898800 vn 0.343200 0.069300 -0.936700 vn 0.401500 0.794100 -0.456400 vn 0.038500 -0.638500 -0.768600 vn 0.401900 0.830200 -0.386400 vn 0.155800 0.946900 0.281200 vn 0.165600 0.960600 0.223100 vn -0.233000 0.609400 0.757900 vn -0.297000 0.673800 0.676600 vn -0.419300 0.731300 0.537900 vn -0.603300 -0.118300 0.788700 vn -0.672500 -0.028200 0.739600 vn -0.628800 -0.732200 0.261700 vn -0.656000 -0.708900 0.259100 vn -0.334600 -0.874300 -0.351600 vn -0.289000 -0.898000 -0.331800 vn 0.488900 -0.526400 -0.695600 vn 0.232600 0.204100 -0.950900 vn 0.274700 0.252500 -0.927800 vn 0.219700 0.882400 -0.416200 vn 0.078900 -0.521900 -0.849300 vn -0.497000 0.866600 0.043800 vn -0.023100 0.971100 0.237700 vn -0.627300 0.487100 0.607600 vn -0.329100 0.583200 0.742700 vn -0.428300 -0.241000 0.870900 vn -0.539100 -0.210400 0.815600 vn 0.034700 -0.838800 0.543300 vn -0.464700 -0.828100 0.313700 vn 0.404700 -0.910700 -0.082300 vn -0.198600 -0.930200 -0.308600 vn 0.223100 0.073400 -0.972000 vn 0.216400 0.130000 -0.967600 vn 0.743000 -0.415900 -0.524300 vn -0.301500 0.637500 -0.709000 vn -0.452800 0.556400 -0.696700 vn -0.610300 0.785400 -0.103800 vn -0.777300 0.614800 -0.133700 vn -0.804100 0.365200 0.469100 vn -0.268500 0.836400 -0.477800 vn -0.610300 0.514300 0.602500 vn -0.313300 -0.123400 0.941600 vn -0.337300 -0.196800 0.920600 vn 0.383200 -0.620100 0.684600 vn 0.314700 -0.692900 0.648800 vn 0.544600 -0.834600 0.083100 vn 0.750400 -0.649900 0.120500 vn 0.622700 -0.562000 -0.544400 vn 0.205300 -0.687000 -0.697000 vn 0.244700 0.105700 -0.963800 vn 0.244800 0.035800 -0.968900 vn -0.023300 0.786700 -0.616900 vn 0.299400 -0.711100 -0.636200 vn 0.105800 0.825100 -0.555000 vn -0.084000 0.993900 0.071100 vn -0.222100 0.974800 0.021800 vn -0.347200 0.684700 0.640800 vn -0.324700 -0.101300 0.940400 vn -0.295700 -0.095200 0.950500 vn -0.170800 -0.817100 0.550600 vn -0.057000 -0.806700 0.588200 vn 0.043800 -0.995200 -0.088000 vn 0.162900 -0.985800 -0.040200 vn 0.072900 -0.645500 -0.760300 vn 0.212400 0.107700 -0.971200 vn -0.002000 0.796000 -0.605300 vn 0.057300 0.811400 -0.581700 vn 0.155100 -0.677100 -0.719400 vn -0.155700 0.986500 0.050100 vn -0.408200 0.709500 0.574500 vn -0.374000 0.685200 0.624900 vn -0.268300 0.676700 0.685600 vn -0.433700 -0.099200 0.895600 vn -0.351500 -0.791100 0.500600 vn -0.281700 -0.806500 0.519900 vn -0.020300 -0.993700 -0.110400 vn 0.303900 -0.672600 0.674700 vn 0.391100 0.040300 0.919400 vn 0.300100 0.042300 0.953000 vn 0.073900 0.699100 0.711200 vn -0.011700 0.665500 0.746300 vn 0.399800 -0.640800 0.655400 vn 0.286300 0.733900 0.615900 vn 0.126500 0.991000 -0.043900 vn -0.178900 0.982700 0.048000 vn -0.087500 0.665900 -0.740900 vn -0.223600 0.693200 -0.685200 vn -0.191700 -0.044600 -0.980400 vn -0.112300 -0.013400 -0.993600 vn -0.121700 -0.707500 -0.696200 vn 0.098300 -0.669200 -0.736600 vn 0.071900 -0.997300 -0.016400 vn 0.247600 -0.966100 -0.073600 vn 0.445700 0.016400 0.895000 vn 0.458100 0.022800 0.888600 vn 0.216500 -0.681400 0.699100 vn 0.565800 0.626100 0.536500 vn 0.536800 0.644600 0.544400 vn 0.448100 0.882700 -0.141600 vn 0.400700 0.907400 -0.127100 vn 0.130600 0.609800 -0.781700 vn 0.148000 0.595200 -0.789800 vn -0.157600 -0.064600 -0.985400 vn -0.153400 -0.054000 -0.986700 vn -0.226500 -0.711400 -0.665300 vn -0.282900 -0.712400 -0.642200 vn -0.102800 -0.994000 0.037700 vn 0.167200 -0.671600 0.721800 vn 0.393700 0.027600 0.918800 vn 0.494600 0.655300 0.570900 vn 0.369200 0.921900 -0.117800 vn 0.077200 0.621200 -0.779800 vn -0.210300 -0.070500 -0.975100 vn 0.477500 0.323900 0.816800 vn 0.227500 0.317500 0.920500 vn -0.228200 -0.895400 -0.382400 vn -0.489000 -0.384500 -0.783000 vn -0.736400 -0.364100 -0.570200 vn -0.713000 0.351500 -0.606700 vn -0.448100 -0.893700 -0.022800 vn -0.621600 0.766500 -0.161400 vn -0.514900 0.836200 -0.189000 vn -0.146200 0.851300 0.503800 vn -0.201500 0.860300 0.468300 vn 0.597900 0.261600 0.757700 vn 0.282700 0.269700 0.920500 vn 0.384800 -0.416700 0.823600 vn 0.673500 -0.574300 0.465500 vn 0.102500 -0.993500 0.049500 vn 0.070600 -0.944500 0.321000 vn -0.546700 -0.420600 -0.724000 vn -0.825200 0.215200 -0.522200 vn -0.717800 0.332000 -0.611900 vn -0.142500 -0.886900 -0.439400 vn -0.797800 0.594700 -0.099100 vn -0.683700 0.691500 0.233000 vn -0.772100 0.238100 0.589200 vn -0.593200 -0.331600 0.733600 vn 0.084100 -0.062700 0.994500 vn -0.242100 -0.815900 0.525200 vn 0.351600 -0.695200 0.626900 vn 0.824000 -0.310600 0.473900 vn 0.728600 0.253900 0.636100 vn 0.300400 0.724500 0.620400 vn -0.309700 0.548800 0.776400 vn 0.114100 -0.979700 -0.164800 vn 0.272900 -0.923100 -0.270800 vn 0.293300 0.638600 0.711400 vn 0.903300 0.297600 0.308900 vn 0.859000 0.324900 0.395700 vn 0.921200 -0.356100 -0.156900 vn 0.197100 0.797500 0.570200 vn 0.913400 -0.287000 -0.288500 vn 0.273900 -0.665100 -0.694700 vn 0.271100 -0.734600 -0.621900 vn -0.366600 -0.750200 -0.550200 vn -0.360100 -0.900900 -0.242400 vn -0.916700 -0.377300 -0.131500 vn -0.904800 -0.403300 -0.136800 vn -0.894000 0.141100 0.425300 vn -0.930600 0.194800 0.309900 vn -0.493300 0.514700 0.701200 vn -0.555100 0.665200 0.499400 vn 0.298900 0.709700 0.637900 vn 0.918400 0.306100 0.250500 vn 0.911800 0.241300 0.332200 vn 0.920300 -0.309500 -0.239500 vn 0.301600 0.558900 0.772500 vn 0.901900 -0.338500 -0.268200 vn 0.326800 -0.826600 -0.458200 vn 0.293700 -0.729600 -0.617600 vn -0.384000 -0.843400 -0.375800 vn -0.394000 -0.748500 -0.533400 vn -0.911000 -0.411900 -0.018700 vn -0.915700 -0.397800 -0.056700 vn -0.915400 0.163300 0.367900 vn -0.880200 0.110500 0.461600 vn -0.413900 -0.688300 -0.595700 vn -0.543300 0.573500 0.613200 vn -0.485600 0.457200 0.745100 vn 0.282900 0.932300 0.225300 vn 0.923000 0.382600 0.040700 vn 0.929600 0.344000 0.132100 vn 0.924200 -0.356700 -0.136100 vn 0.294900 0.859600 0.417200 vn 0.916400 -0.371200 -0.149400 vn 0.333200 -0.923200 -0.191400 vn 0.327400 -0.901600 -0.282800 vn -0.375400 -0.900400 -0.220000 vn -0.916200 -0.399600 0.029100 vn -0.912400 -0.409200 0.006300 vn -0.946500 0.271600 0.174400 vn -0.940100 0.237200 0.244800 vn -0.589300 0.766600 0.255200 vn -0.579600 0.709600 0.400600 vn 0.921700 0.359500 0.145700 vn 0.919800 -0.371600 0.125800 vn 0.310000 0.893500 0.324900 vn 0.355200 -0.932800 0.060300 vn -0.354500 -0.924400 0.140800 vn -0.373200 -0.919100 -0.126600 vn -0.889000 -0.405900 0.212000 vn -0.917300 0.267300 0.295100 vn -0.558400 0.764400 0.322300 vn 0.803200 -0.594400 -0.039200 vn 0.984000 0.090900 0.153100 vn 0.686400 0.725100 -0.055600 vn 0.658700 0.752400 -0.006900 vn 0.748100 -0.655900 0.100600 vn -0.006300 0.971200 -0.238100 vn -0.595300 0.640200 -0.485500 vn -0.643000 0.623800 -0.444200 vn -0.879100 -0.113800 -0.462900 vn -0.541900 -0.709700 -0.450100 vn -0.569100 -0.749500 -0.338300 vn 0.085400 -0.992700 -0.084700 vn -0.063700 -0.640600 0.765200 vn 0.707100 0.000000 0.707100 vn -0.571100 0.745800 -0.342900 vn -0.040500 0.855500 0.516300 vn -0.011100 -0.856200 0.516600 vn -0.457800 -0.725200 -0.514400 usemtl None s 1 f 2/1/1 3/2/2 4/3/3 f 6/4/4 3/2/2 2/1/1 f 8/5/5 3/2/2 6/4/4 f 9/6/6 10/7/7 3/2/2 f 4/3/3 3/2/2 10/7/7 f 13/8/8 14/9/9 15/10/10 f 17/11/11 14/9/9 13/8/8 f 19/12/12 14/9/9 17/11/11 f 20/13/13 21/14/14 14/9/9 f 15/10/10 14/9/9 21/14/14 f 23/15/15 24/16/16 6/4/4 f 15/10/10 24/16/16 23/15/15 f 25/17/17 24/16/16 15/10/10 f 26/18/18 27/19/19 24/16/16 f 6/4/4 24/16/16 27/19/19 f 28/20/20 29/21/21 17/11/11 f 4/22/3 29/21/21 28/20/20 f 30/23/22 29/21/21 4/22/3 f 31/24/23 32/25/24 29/21/21 f 17/11/11 29/21/21 32/25/24 f 33/26/25 34/27/26 35/28/27 f 38/29/28 35/28/27 34/27/26 f 39/30/29 40/31/30 35/28/27 f 41/32/31 36/33/32 35/28/27 f 43/34/33 44/35/34 45/36/35 f 47/37/36 44/35/34 43/34/33 f 49/38/37 44/35/34 47/37/36 f 45/36/35 44/35/34 49/38/37 f 18/39/38 51/40/39 52/41/40 f 54/42/41 52/41/40 51/40/39 f 56/43/42 52/41/40 54/42/41 f 19/12/12 52/41/40 56/43/42 f 57/44/43 58/45/44 59/46/45 f 61/47/46 62/48/47 59/46/45 f 64/49/48 59/46/45 62/48/47 f 65/50/49 60/51/50 59/46/45 f 66/52/51 67/53/52 68/54/53 f 70/55/54 71/56/55 68/54/53 f 72/57/56 73/58/57 68/54/53 f 74/59/58 69/60/59 68/54/53 f 75/61/60 76/62/61 77/63/62 f 79/64/63 80/65/64 77/63/62 f 82/66/65 77/63/62 80/65/64 f 83/67/66 78/68/67 77/63/62 f 84/69/68 85/70/69 86/71/70 f 89/72/71 86/71/70 85/70/69 f 5/73/72 2/1/1 86/71/70 f 1/74/73 87/75/74 86/71/70 f 90/76/75 91/77/76 89/72/71 f 93/78/77 91/77/76 90/76/75 f 12/79/78 23/15/15 91/77/76 f 5/73/72 89/72/71 91/77/76 f 94/80/79 95/81/80 93/78/77 f 97/82/81 95/81/80 94/80/79 f 16/83/82 13/8/8 95/81/80 f 12/79/78 93/78/77 95/81/80 f 96/84/83 98/85/84 99/86/85 f 87/87/74 99/86/85 98/85/84 f 1/88/73 28/20/20 99/86/85 f 16/83/82 97/82/81 99/86/85 f 100/89/68 101/90/86 102/91/87 f 104/92/71 105/93/71 102/91/87 f 85/70/69 102/91/87 105/93/71 f 103/94/88 102/91/87 85/70/69 f 106/95/89 107/96/75 105/93/71 f 108/97/90 109/98/90 107/96/75 f 90/76/75 107/96/75 109/98/90 f 88/99/91 105/93/71 107/96/75 f 110/100/79 111/101/79 109/98/90 f 113/102/92 111/101/79 110/100/79 f 96/84/83 94/80/79 111/101/79 f 109/98/90 111/101/79 94/80/79 f 112/103/93 114/104/94 115/105/95 f 103/106/88 115/105/95 114/104/94 f 84/107/68 98/85/84 115/105/95 f 113/102/92 115/105/95 98/85/84 f 116/108/96 117/109/97 118/110/98 f 121/111/99 118/110/98 117/109/97 f 101/90/86 118/110/98 121/111/99 f 119/112/88 118/110/98 101/90/86 f 122/113/100 123/114/75 121/111/99 f 124/115/90 125/116/101 123/114/75 f 106/95/89 123/114/75 125/116/101 f 121/111/99 123/114/75 106/95/89 f 126/117/102 127/118/103 125/116/101 f 129/119/104 127/118/103 126/117/102 f 112/103/93 110/100/79 127/118/103 f 108/97/90 125/116/101 127/118/103 f 128/120/93 130/121/105 131/122/106 f 119/123/88 131/122/106 130/121/105 f 114/104/94 131/122/106 119/123/88 f 129/119/104 131/122/106 114/104/94 f 74/59/58 73/58/57 132/124/107 f 72/57/56 134/125/108 132/124/107 f 117/109/97 132/126/69 134/127/109 f 133/128/110 132/126/69 117/109/97 f 72/57/56 71/56/55 135/129/111 f 70/55/54 136/130/112 135/129/111 f 122/113/100 135/131/75 136/132/113 f 134/127/109 135/131/75 122/113/100 f 70/55/54 67/53/52 137/133/114 f 66/52/51 138/134/115 137/133/114 f 126/117/102 137/135/79 138/136/116 f 136/132/113 137/135/79 126/117/102 f 66/52/51 69/60/59 139/137/117 f 74/59/58 133/138/118 139/137/117 f 116/139/96 130/121/105 139/140/94 f 138/136/116 139/140/94 130/121/105 f 75/61/60 140/141/119 141/142/120 f 143/143/121 141/142/120 140/141/119 f 55/144/122 144/145/123 141/142/120 f 76/62/61 141/142/120 144/145/123 f 145/146/124 146/147/125 147/148/126 f 150/149/127 147/148/126 146/147/125 f 151/150/128 152/151/129 147/148/126 f 83/67/66 148/152/130 147/148/126 f 153/153/131 154/154/132 155/155/133 f 158/156/134 155/155/133 154/154/132 f 159/157/135 160/158/136 155/159/133 f 161/160/137 156/161/138 155/159/133 f 162/162/139 163/163/140 164/164/141 f 166/165/142 164/164/141 163/163/140 f 167/166/143 168/167/144 164/164/141 f 165/168/145 164/164/141 168/167/144 f 171/169/146 172/170/147 173/171/148 f 174/172/149 175/173/150 172/170/147 f 176/174/151 177/175/152 172/170/147 f 173/171/148 172/170/147 177/175/152 f 179/176/153 180/177/154 181/178/155 f 184/179/156 181/178/155 180/177/154 f 185/180/157 186/181/158 181/178/155 f 182/182/159 181/178/155 186/181/158 f 162/162/139 165/168/145 188/183/160 f 169/184/161 190/185/162 188/183/160 f 80/65/64 188/183/160 190/185/162 f 79/64/63 189/186/163 188/183/160 f 163/163/140 191/187/164 54/42/41 f 162/162/139 189/186/163 191/187/164 f 79/64/63 144/145/123 191/187/164 f 54/42/41 191/187/164 144/145/123 f 146/147/125 192/188/165 193/189/166 f 145/146/124 194/190/167 192/188/165 f 161/160/137 160/158/136 192/188/165 f 159/157/135 193/189/166 192/188/165 f 195/191/168 196/192/169 197/193/170 f 199/194/171 200/195/172 197/193/170 f 201/196/173 202/197/174 197/193/170 f 203/198/175 198/199/176 197/193/170 f 204/200/177 205/201/178 206/202/179 f 208/203/180 209/204/181 206/202/179 f 211/205/182 206/202/179 209/204/181 f 212/206/183 207/207/184 206/202/179 f 194/190/167 213/208/185 214/209/186 f 145/146/124 148/152/130 213/208/185 f 82/66/65 213/208/185 148/152/130 f 81/210/187 214/209/186 213/208/185 f 140/141/119 215/211/188 216/212/189 f 75/61/60 78/68/67 215/211/188 f 152/151/129 215/211/188 78/68/67 f 151/150/128 216/212/189 215/211/188 f 157/213/190 154/154/132 217/214/191 f 219/215/192 217/214/191 154/154/132 f 169/184/161 168/167/144 217/214/191 f 218/216/193 217/214/191 168/167/144 f 153/153/131 156/217/138 220/218/194 f 214/209/186 220/219/194 156/161/138 f 81/210/187 190/185/162 220/219/194 f 219/215/192 220/218/194 190/185/162 f 221/220/195 222/221/196 223/222/197 f 226/223/198 223/222/197 222/221/196 f 9/6/6 227/224/199 223/222/197 f 224/225/200 223/222/197 227/224/199 f 225/226/201 228/227/202 229/228/203 f 230/229/204 231/230/205 229/231/203 f 11/232/206 10/233/7 229/231/203 f 226/223/198 229/228/203 10/7/7 f 232/234/207 233/235/208 231/230/205 f 234/236/209 235/237/210 233/235/208 f 236/238/211 233/235/208 235/237/210 f 11/232/206 231/230/205 233/235/208 f 237/239/212 238/240/213 235/237/210 f 224/225/200 238/241/213 237/242/212 f 158/243/134 238/241/213 224/225/200 f 235/237/210 238/240/213 158/156/134 f 240/244/214 241/245/215 242/246/216 f 243/247/217 244/248/218 241/245/215 f 225/249/201 222/250/196 241/245/215 f 242/246/216 241/245/215 222/250/196 f 243/247/217 245/251/219 246/252/220 f 248/253/221 246/252/220 245/251/219 f 228/254/202 246/252/220 248/253/221 f 244/248/218 246/252/220 228/254/202 f 247/255/222 249/256/223 250/257/224 f 252/258/225 250/257/224 249/256/223 f 232/234/207 250/257/224 252/258/225 f 230/229/204 248/253/221 250/257/224 f 253/259/226 254/260/227 252/258/225 f 239/261/228 242/246/216 254/260/227 f 221/262/195 237/239/212 254/260/227 f 234/236/209 252/258/225 254/260/227 f 256/263/229 257/264/230 258/265/231 f 259/266/232 260/267/233 257/264/230 f 240/244/214 257/264/230 260/267/233 f 239/261/228 258/265/231 257/264/230 f 261/268/234 262/269/235 260/267/233 f 264/270/236 262/269/235 261/268/234 f 247/255/222 245/251/219 262/269/235 f 243/247/217 260/267/233 262/269/235 f 263/271/237 265/272/238 266/273/239 f 268/274/240 266/273/239 265/272/238 f 251/275/241 249/256/223 266/273/239 f 264/270/236 266/273/239 249/256/223 f 267/276/242 269/277/243 270/278/244 f 255/279/245 258/265/231 270/278/244 f 253/259/226 270/278/244 258/265/231 f 268/274/240 270/278/244 253/259/226 f 271/280/246 272/281/247 273/282/248 f 276/283/249 273/282/248 272/281/247 f 167/166/143 277/284/250 273/282/248 f 274/285/251 273/282/248 277/284/250 f 275/286/252 278/287/253 279/288/254 f 280/289/255 281/290/256 279/288/254 f 218/216/193 279/288/254 281/290/256 f 276/283/249 279/288/254 218/216/193 f 282/291/257 283/292/258 281/290/256 f 284/293/259 285/294/260 283/292/258 f 236/238/211 283/292/258 285/294/260 f 157/213/190 281/290/256 283/292/258 f 286/295/261 287/296/262 285/294/260 f 274/285/251 287/296/262 286/295/261 f 30/23/22 287/296/262 274/285/251 f 285/294/260 287/296/262 30/23/22 f 288/297/263 289/298/264 290/299/265 f 293/300/266 290/299/265 289/298/264 f 275/286/252 272/281/247 290/299/265 f 291/301/267 290/299/265 272/281/247 f 292/302/268 294/303/269 295/304/270 f 297/305/271 295/304/270 294/303/269 f 278/287/253 295/304/270 297/305/271 f 293/300/266 295/304/270 278/287/253 f 298/306/272 299/307/273 300/308/274 f 302/309/275 303/310/276 300/308/274 f 305/311/277 300/308/274 303/310/276 f 301/312/278 300/308/274 305/311/277 f 308/313/279 309/314/280 310/315/281 f 291/301/267 309/314/280 308/313/279 f 271/280/246 286/295/261 309/314/280 f 284/293/259 310/315/281 309/314/280 f 312/316/282 313/317/283 314/318/284 f 315/319/285 316/320/286 313/317/283 f 282/291/257 313/317/283 316/320/286 f 280/289/255 314/318/284 313/317/283 f 317/321/287 318/322/288 316/320/286 f 320/323/289 318/322/288 317/321/287 f 310/315/281 318/322/288 320/323/289 f 316/320/286 318/322/288 310/315/281 f 319/324/290 321/325/291 322/326/292 f 324/327/293 322/326/292 321/325/291 f 296/328/294 325/329/295 322/326/292 f 320/323/289 322/326/292 325/329/295 f 326/330/296 327/331/297 324/327/293 f 311/332/298 314/318/284 327/331/297 f 297/305/271 327/331/297 314/318/284 f 324/327/293 327/331/297 297/305/271 f 328/333/299 329/334/300 330/335/301 f 332/336/302 333/337/303 330/335/301 f 312/316/282 330/335/301 333/337/303 f 311/332/298 331/338/304 330/335/301 f 332/336/302 334/339/305 335/340/306 f 337/341/307 335/340/306 334/339/305 f 317/321/287 335/340/306 337/341/307 f 315/319/285 333/337/303 335/340/306 f 339/342/308 340/343/309 341/344/310 f 342/345/311 343/346/312 340/343/309 f 345/347/313 340/343/309 343/346/312 f 341/344/310 340/343/309 345/347/313 f 348/348/314 349/349/315 350/350/316 f 328/333/299 331/338/304 349/349/315 f 311/332/298 326/330/296 349/349/315 f 350/350/316 349/349/315 326/330/296 f 203/198/175 202/197/174 351/351/317 f 201/196/173 353/352/318 351/351/317 f 355/353/319 351/351/317 353/352/318 f 302/309/275 352/354/320 351/351/317 f 201/196/173 200/195/172 356/355/321 f 199/194/171 357/356/322 356/355/321 f 358/357/323 359/358/324 356/355/321 f 354/359/325 353/352/318 356/355/321 f 199/194/171 196/192/169 360/360/326 f 195/191/168 361/361/327 360/360/326 f 298/306/272 362/362/328 360/360/326 f 357/356/322 360/360/326 362/362/328 f 195/191/168 198/199/176 363/363/329 f 203/198/175 352/354/320 363/363/329 f 299/307/273 363/363/329 352/354/320 f 361/361/327 363/363/329 299/307/273 f 365/364/330 366/365/331 367/366/332 f 369/367/333 366/365/331 365/364/330 f 323/368/334 321/325/291 366/365/331 f 367/366/332 366/365/331 321/325/291 f 370/369/335 371/370/336 369/367/333 f 373/371/337 371/370/336 370/369/335 f 350/350/316 371/370/336 373/371/337 f 369/367/333 371/370/336 350/350/316 f 372/372/338 374/373/339 375/374/340 f 377/375/341 375/374/340 374/373/339 f 378/376/342 375/374/340 377/375/341 f 373/371/337 375/374/340 378/376/342 f 379/377/343 380/378/344 377/375/341 f 367/366/332 380/378/344 379/377/343 f 337/341/307 380/378/344 367/366/332 f 377/375/341 380/378/344 337/341/307 f 382/379/345 383/380/346 384/381/347 f 385/382/348 386/383/349 383/380/346 f 387/384/350 388/385/351 383/380/346 f 389/386/352 384/381/347 383/380/346 f 343/346/312 390/387/353 391/388/354 f 392/389/355 390/387/353 343/346/312 f 372/372/338 370/369/335 390/387/353 f 391/388/354 390/387/353 370/369/335 f 394/390/356 395/391/357 396/392/358 f 398/393/359 395/391/357 394/390/356 f 400/394/360 395/391/357 398/393/359 f 396/392/358 395/391/357 400/394/360 f 338/395/361 341/344/310 402/396/362 f 404/397/363 402/396/362 341/344/310 f 379/377/343 402/396/362 404/397/363 f 376/398/364 403/399/365 402/396/362 f 405/400/366 406/401/367 407/402/368 f 408/403/369 409/404/370 406/401/367 f 410/405/371 406/401/367 409/404/370 f 26/18/18 407/402/368 406/401/367 f 411/406/372 412/407/373 409/404/370 f 414/408/374 412/407/373 411/406/372 f 216/212/189 412/407/373 414/408/374 f 409/404/370 412/407/373 216/212/189 f 413/409/375 415/410/376 416/411/377 f 417/412/378 416/411/377 415/410/376 f 418/413/379 416/411/377 417/412/378 f 414/408/374 416/411/377 418/413/379 f 187/414/380 186/181/158 419/415/381 f 185/180/157 407/402/368 419/415/381 f 25/17/17 419/415/381 407/402/368 f 22/416/382 417/412/378 419/415/381 f 420/417/383 421/418/384 422/419/385 f 425/420/386 422/419/385 421/418/384 f 426/421/387 427/422/388 422/419/385 f 423/423/389 422/419/385 427/422/388 f 424/424/390 429/425/391 430/426/392 f 431/427/393 432/428/394 430/426/392 f 433/429/395 434/430/396 430/426/392 f 425/420/386 430/426/392 434/430/396 f 435/431/397 436/432/398 432/428/394 f 437/433/399 438/434/400 436/432/398 f 440/435/401 436/432/398 438/434/400 f 433/429/395 432/428/394 436/432/398 f 441/436/402 442/437/403 438/434/400 f 420/417/383 423/423/389 442/437/403 f 443/438/404 442/437/403 423/423/389 f 439/439/405 438/434/400 442/437/403 f 444/440/406 445/441/407 446/442/408 f 449/443/409 446/442/408 445/441/407 f 424/424/390 421/418/384 446/442/408 f 447/444/410 446/442/408 421/418/384 f 448/445/411 450/446/412 451/447/413 f 452/448/414 453/449/415 451/447/413 f 431/427/393 429/425/391 451/447/413 f 449/443/409 451/447/413 429/425/391 f 454/450/416 455/451/417 453/449/415 f 457/452/418 455/451/417 454/450/416 f 435/431/397 455/451/417 457/452/418 f 431/427/393 453/449/415 455/451/417 f 456/453/419 458/454/420 459/455/421 f 447/444/410 459/455/421 458/454/420 f 420/417/383 441/436/402 459/455/421 f 437/433/399 457/452/418 459/455/421 f 389/386/352 388/385/351 460/456/422 f 387/384/350 462/457/423 460/456/422 f 365/364/330 460/456/422 462/457/423 f 461/458/424 460/456/422 365/364/330 f 387/384/350 386/383/349 463/459/425 f 464/460/426 463/459/425 386/383/349 f 344/461/427 391/388/354 463/459/425 f 462/457/423 463/459/425 391/388/354 f 466/462/428 467/463/429 468/464/430 f 470/465/431 467/463/432 466/462/428 f 472/466/433 467/463/432 470/465/431 f 468/464/430 467/463/429 472/466/433 f 384/381/347 474/467/434 475/468/435 f 389/386/352 461/458/424 474/467/434 f 404/397/363 474/467/434 461/458/424 f 346/469/436 475/468/435 474/467/434 f 476/470/437 477/471/438 478/472/439 f 481/473/440 478/472/439 477/471/438 f 376/398/364 374/373/339 478/472/439 f 479/474/441 478/472/439 374/373/339 f 480/475/442 482/476/443 483/477/444 f 485/478/445 483/477/444 482/476/443 f 403/399/365 483/477/444 485/478/445 f 481/473/440 483/477/444 403/399/365 f 486/479/446 487/480/447 485/478/445 f 488/481/448 489/482/449 487/480/447 f 339/342/308 487/480/447 489/482/449 f 338/395/361 485/478/445 487/480/447 f 490/483/450 491/484/451 489/482/449 f 476/470/437 479/474/441 491/484/451 f 392/389/355 491/484/451 479/474/441 f 342/345/311 489/482/449 491/484/451 f 493/485/452 494/486/453 495/487/454 f 496/488/455 497/489/456 494/486/453 f 477/471/438 494/486/453 497/489/456 f 476/470/437 495/487/454 494/486/453 f 498/490/457 499/491/458 497/489/456 f 501/492/459 499/491/458 498/490/457 f 484/493/460 482/476/443 499/491/458 f 497/489/456 499/491/458 482/476/443 f 500/494/461 502/495/462 503/496/463 f 505/497/464 503/496/463 502/495/462 f 488/481/448 486/479/446 503/496/463 f 501/492/459 503/496/463 486/479/446 f 504/498/465 506/499/466 507/500/467 f 492/501/468 495/487/454 507/500/467 f 490/483/450 507/500/467 495/487/454 f 505/497/464 507/500/467 490/483/450 f 509/502/469 510/503/470 511/504/471 f 512/505/472 513/506/473 510/503/470 f 277/284/250 510/503/470 513/506/473 f 511/504/471 510/503/470 277/284/250 f 514/507/474 515/508/475 513/506/473 f 516/509/476 517/510/477 515/508/475 f 18/39/38 32/25/24 515/508/475 f 513/506/473 515/508/475 32/25/24 f 518/511/478 519/512/479 517/510/477 f 520/513/480 521/514/481 519/512/479 f 51/40/39 519/512/479 521/514/481 f 517/510/477 519/512/479 51/40/39 f 522/515/482 523/516/483 521/514/481 f 508/517/484 511/504/471 523/516/483 f 166/165/142 523/516/483 511/504/471 f 521/514/481 523/516/483 166/165/142 f 524/518/485 525/519/486 526/520/487 f 529/521/488 526/520/487 525/519/486 f 512/505/472 509/502/469 526/520/487 f 527/522/489 526/520/487 509/502/469 f 528/523/490 530/524/491 531/525/492 f 532/526/493 533/527/494 531/525/492 f 514/507/474 531/525/492 533/527/494 f 529/521/488 531/525/492 514/507/474 f 534/528/495 535/529/496 533/527/494 f 536/530/497 537/531/498 535/529/496 f 518/511/478 535/529/496 537/531/498 f 516/509/476 533/527/494 535/529/496 f 538/532/499 539/533/500 537/531/498 f 527/522/489 539/533/500 538/532/499 f 508/517/484 522/515/482 539/533/500 f 520/513/480 537/531/498 539/533/500 f 540/534/501 541/535/502 542/536/503 f 544/537/504 545/538/505 542/536/503 f 546/539/506 547/540/507 542/536/503 f 548/541/508 543/542/509 542/536/503 f 62/48/47 549/543/510 550/544/511 f 61/47/46 551/545/512 549/543/510 f 294/303/269 549/543/510 551/545/512 f 550/544/511 549/543/510 294/303/269 f 61/47/46 58/45/44 552/546/513 f 57/44/43 553/547/514 552/546/513 f 325/329/295 552/546/513 553/547/514 f 551/545/512 552/546/513 325/329/295 f 57/44/43 60/51/50 554/548/515 f 555/549/516 554/548/515 60/51/50 f 308/313/279 554/548/515 555/549/516 f 553/547/514 554/548/515 308/313/279 f 548/541/508 547/540/507 556/550/517 f 546/539/506 558/551/518 556/550/517 f 289/298/264 556/550/517 558/551/518 f 557/552/519 556/550/517 289/298/264 f 546/539/506 545/538/505 559/553/520 f 560/554/521 559/553/520 545/538/505 f 63/555/522 550/544/511 559/553/520 f 558/551/518 559/553/520 550/544/511 f 561/556/523 562/557/524 563/558/525 f 565/559/526 566/560/527 563/558/525 f 567/561/528 568/562/529 563/558/525 f 569/563/530 564/564/531 563/558/525 f 543/542/509 570/565/532 571/566/533 f 548/541/508 557/552/519 570/565/532 f 555/549/516 570/565/532 557/552/519 f 65/50/49 571/566/533 570/565/532 f 573/567/534 574/568/535 575/569/536 f 577/570/537 574/568/535 573/567/534 f 525/519/486 574/568/535 577/570/537 f 524/518/485 575/569/536 574/568/535 f 578/571/538 579/572/539 577/570/537 f 581/573/540 579/572/539 578/571/538 f 532/526/493 530/524/491 579/572/539 f 528/523/490 577/570/537 579/572/539 f 582/574/541 583/575/542 581/573/540 f 585/576/543 583/575/542 582/574/541 f 536/530/497 534/528/495 583/575/542 f 581/573/540 583/575/542 534/528/495 f 586/577/544 587/578/545 585/576/543 f 575/569/536 587/578/545 586/577/544 f 538/532/499 587/578/545 575/569/536 f 585/576/543 587/578/545 538/532/499 f 588/579/546 589/580/547 590/581/548 f 592/582/549 593/583/550 590/581/548 f 594/584/551 595/585/552 590/581/548 f 596/586/553 591/587/554 590/581/548 f 209/204/181 597/588/555 598/589/556 f 599/590/557 597/588/555 209/204/181 f 580/591/558 578/571/538 597/588/555 f 576/592/559 598/589/556 597/588/555 f 600/593/560 601/594/561 602/595/562 f 604/596/563 605/597/564 602/595/562 f 606/598/565 607/599/566 602/595/562 f 608/600/567 603/601/568 602/595/562 f 207/207/184 609/602/569 610/603/570 f 611/604/571 609/602/569 207/207/184 f 572/605/572 586/577/544 609/602/569 f 584/606/573 610/603/570 609/602/569 f 613/607/574 614/608/575 615/609/576 f 617/610/577 614/608/575 613/607/574 f 576/592/559 573/567/534 614/608/575 f 615/609/576 614/608/575 573/567/534 f 616/611/578 618/612/579 619/613/580 f 621/614/581 619/613/580 618/612/579 f 210/615/582 598/589/556 619/613/580 f 617/610/577 619/613/580 598/589/556 f 620/616/583 622/617/584 623/618/585 f 624/619/586 625/620/587 623/618/585 f 211/205/182 623/618/585 625/620/587 f 210/615/582 621/614/581 623/618/585 f 626/621/588 627/622/589 625/620/587 f 612/623/590 615/609/576 627/622/589 f 611/604/571 627/622/589 615/609/576 f 212/206/183 625/620/587 627/622/589 f 427/422/388 628/624/591 629/625/592 f 630/626/593 628/624/591 427/422/388 f 582/574/541 628/624/591 630/626/593 f 580/591/558 629/625/592 628/624/591 f 426/421/387 434/430/396 631/627/594 f 632/628/595 631/627/594 434/430/396 f 204/200/177 610/603/570 631/627/594 f 630/626/593 631/627/594 610/603/570 f 440/435/401 633/629/596 632/628/595 f 439/439/405 634/630/597 633/629/596 f 208/203/180 205/201/178 633/629/596 f 632/628/595 633/629/596 205/201/178 f 443/438/404 635/631/598 634/630/597 f 428/632/599 629/625/592 635/631/598 f 599/590/557 635/631/598 629/625/592 f 208/203/180 634/630/597 635/631/598 f 637/633/600 638/634/601 639/635/602 f 640/636/603 641/637/604 638/634/601 f 613/607/574 638/634/601 641/637/604 f 612/623/590 639/635/602 638/634/601 f 640/636/603 642/638/605 643/639/606 f 645/640/607 643/639/606 642/638/605 f 620/616/583 618/612/579 643/639/606 f 641/637/604 643/639/606 618/612/579 f 644/641/608 646/642/609 647/643/610 f 649/644/611 647/643/610 646/642/609 f 624/619/586 622/617/584 647/643/610 f 645/640/607 647/643/610 622/617/584 f 648/645/612 650/646/613 651/647/614 f 639/635/602 651/647/614 650/646/613 f 626/621/588 651/647/614 639/635/602 f 624/619/586 649/644/611 651/647/614 f 653/648/615 654/649/616 655/650/617 f 656/651/618 657/652/619 654/649/616 f 637/633/600 654/649/616 657/652/619 f 636/653/620 655/650/617 654/649/616 f 658/654/621 659/655/622 657/652/619 f 661/656/623 659/655/622 658/654/621 f 644/641/608 642/638/605 659/655/622 f 640/636/603 657/652/619 659/655/622 f 660/657/624 662/658/625 663/659/626 f 665/660/627 663/659/626 662/658/625 f 648/645/612 646/642/609 663/659/626 f 661/656/623 663/659/626 646/642/609 f 664/661/628 666/662/629 667/663/630 f 655/650/617 667/663/630 666/662/629 f 636/653/620 650/646/613 667/663/630 f 665/660/627 667/663/630 650/646/613 f 668/664/631 669/665/632 670/666/633 f 673/667/634 670/666/633 669/665/632 f 64/49/48 670/666/633 673/667/634 f 63/555/522 671/668/635 670/666/633 f 672/669/636 674/670/637 675/671/638 f 677/672/639 675/671/638 674/670/637 f 540/534/501 571/566/533 675/671/638 f 673/667/634 675/671/638 571/566/533 f 676/673/640 678/674/641 679/675/642 f 680/676/643 681/677/644 679/675/642 f 541/535/502 679/675/642 681/677/644 f 677/672/639 679/675/642 541/535/502 f 682/678/645 683/679/646 681/677/644 f 668/664/631 671/668/635 683/679/646 f 560/554/521 683/679/646 671/668/635 f 544/537/504 681/677/644 683/679/646 f 685/680/647 686/681/648 687/682/649 f 688/683/650 689/684/651 686/681/648 f 672/669/636 669/665/632 686/681/648 f 687/682/649 686/681/648 669/665/632 f 688/683/650 690/685/652 691/686/653 f 693/687/654 691/686/653 690/685/652 f 676/673/640 674/670/637 691/686/653 f 689/684/651 691/686/653 674/670/637 f 692/688/655 694/689/656 695/690/657 f 697/691/658 695/690/657 694/689/656 f 678/674/641 695/690/657 697/691/658 f 693/687/654 695/690/657 678/674/641 f 698/692/659 699/693/660 697/691/658 f 684/694/661 687/682/649 699/693/660 f 682/678/645 699/693/660 687/682/649 f 680/676/643 697/691/658 699/693/660 f 700/695/662 701/696/663 702/697/664 f 705/698/665 702/697/664 701/696/663 f 685/680/647 702/697/664 705/698/665 f 684/694/661 703/699/666 702/697/664 f 704/700/667 706/701/668 707/702/669 f 708/703/670 709/704/671 707/702/669 f 692/688/655 690/685/652 707/702/669 f 705/698/665 707/702/669 690/685/652 f 710/705/672 711/706/673 709/704/671 f 712/707/674 713/708/675 711/706/673 f 694/689/656 711/706/673 713/708/675 f 709/704/671 711/706/673 694/689/656 f 714/709/676 715/710/677 713/708/675 f 703/699/666 715/710/677 714/709/676 f 698/692/659 715/710/677 703/699/666 f 696/711/678 713/708/675 715/710/677 f 716/712/679 717/713/680 718/714/681 f 721/715/682 718/714/681 717/713/680 f 656/651/618 653/648/615 718/714/681 f 719/716/683 718/714/681 653/648/615 f 720/717/684 722/718/685 723/719/686 f 724/720/687 725/721/688 723/719/686 f 660/657/624 658/654/621 723/719/686 f 721/715/682 723/719/686 658/654/621 f 726/722/689 727/723/690 725/721/688 f 728/724/691 729/725/692 727/723/690 f 662/658/625 727/723/690 729/725/692 f 660/657/624 725/721/688 727/723/690 f 728/724/691 730/726/693 731/727/694 f 719/716/683 731/727/694 730/726/693 f 652/728/695 666/662/629 731/727/694 f 729/725/692 731/727/694 666/662/629 f 733/729/696 734/730/697 735/731/698 f 736/732/699 737/733/700 734/730/697 f 410/405/371 734/730/697 737/733/700 f 151/150/128 735/731/698 734/730/697 f 738/734/701 739/735/702 737/733/700 f 741/736/703 739/735/702 738/734/701 f 7/737/704 27/19/19 739/735/702 f 737/733/700 739/735/702 27/19/19 f 740/738/705 742/739/706 743/740/707 f 745/741/708 743/740/707 742/739/706 f 149/742/709 746/743/710 743/740/707 f 741/736/703 743/740/707 746/743/710 f 747/744/711 748/745/712 745/741/708 f 732/746/713 735/731/698 748/745/712 f 150/149/127 748/745/712 735/731/698 f 745/741/708 748/745/712 150/149/127 f 750/747/714 751/748/715 752/749/716 f 753/750/717 754/751/718 751/748/715 f 56/43/42 751/748/715 754/751/718 f 55/144/122 752/749/716 751/748/715 f 755/752/719 756/753/720 754/751/718 f 757/754/721 758/755/722 756/753/720 f 22/416/382 21/14/14 756/753/720 f 754/751/718 756/753/720 21/14/14 f 757/754/721 759/756/723 760/757/724 f 762/758/725 760/757/724 759/756/723 f 142/759/726 418/413/379 760/757/724 f 758/755/722 760/757/724 418/413/379 f 761/760/727 763/761/728 764/762/729 f 752/749/716 764/762/729 763/761/728 f 143/143/121 764/762/729 752/749/716 f 762/758/725 764/762/729 143/143/121 f 766/763/730 767/764/731 768/765/732 f 769/766/733 770/767/734 767/764/731 f 704/700/667 701/696/663 767/764/731 f 768/765/732 767/764/731 701/696/663 f 771/768/735 772/769/736 770/767/734 f 774/770/737 772/769/736 771/768/735 f 706/701/668 772/769/736 774/770/737 f 770/767/734 772/769/736 706/701/668 f 773/771/738 775/772/739 776/773/740 f 778/774/741 776/773/740 775/772/739 f 710/705/672 776/773/740 778/774/741 f 708/703/670 774/770/737 776/773/740 f 777/775/742 779/776/743 780/777/744 f 765/778/745 768/765/732 780/777/744 f 700/695/662 714/709/676 780/777/744 f 712/707/674 778/774/741 780/777/744 f 782/779/746 783/780/747 784/781/748 f 785/782/749 786/783/750 783/780/747 f 766/763/730 783/780/747 786/783/750 f 765/778/745 784/781/748 783/780/747 f 787/784/751 788/785/752 786/783/750 f 790/786/753 788/785/752 787/784/751 f 773/771/738 771/768/735 788/785/752 f 769/766/733 786/783/750 788/785/752 f 789/787/754 791/788/755 792/789/756 f 794/790/757 792/789/756 791/788/755 f 777/775/742 775/772/739 792/789/756 f 790/786/753 792/789/756 775/772/739 f 793/791/758 795/792/759 796/793/760 f 781/794/761 784/781/748 796/793/760 f 779/776/743 796/793/760 784/781/748 f 794/790/757 796/793/760 779/776/743 f 798/795/762 799/796/763 800/797/764 f 801/798/765 802/799/766 799/796/763 f 782/779/746 799/796/763 802/799/766 f 781/794/761 800/797/764 799/796/763 f 803/800/767 804/801/768 802/799/766 f 806/802/769 804/801/768 803/800/767 f 789/787/754 787/784/751 804/801/768 f 785/782/749 802/799/766 804/801/768 f 805/803/770 807/804/771 808/805/772 f 810/806/773 808/805/772 807/804/771 f 793/791/758 791/788/755 808/805/772 f 806/802/769 808/805/772 791/788/755 f 809/807/774 811/808/775 812/809/776 f 797/810/777 800/797/764 812/809/776 f 795/792/759 812/809/776 800/797/764 f 810/806/773 812/809/776 795/792/759 f 569/563/530 568/562/529 813/811/778 f 567/561/528 815/812/779 813/811/778 f 798/795/762 813/811/778 815/812/779 f 797/810/777 814/813/780 813/811/778 f 567/561/528 566/560/527 816/814/781 f 565/559/526 817/815/782 816/814/781 f 805/803/770 803/800/767 816/814/781 f 801/798/765 815/812/779 816/814/781 f 565/559/526 562/557/524 818/816/783 f 561/556/523 819/817/784 818/816/783 f 809/807/774 807/804/771 818/816/783 f 817/815/782 818/816/783 807/804/771 f 561/556/523 564/564/531 820/818/785 f 569/563/530 814/813/780 820/818/785 f 811/808/775 820/818/785 814/813/780 f 819/817/784 820/818/785 811/808/775 f 596/586/553 595/585/552 821/819/786 f 594/584/551 823/820/787 821/819/786 f 717/713/680 821/819/786 823/820/787 f 716/712/679 822/821/788 821/819/786 f 594/584/551 593/583/550 824/822/789 f 592/582/549 825/823/790 824/822/789 f 722/718/685 824/822/789 825/823/790 f 823/820/787 824/822/789 722/718/685 f 592/582/549 589/580/547 826/824/791 f 588/579/546 827/825/792 826/824/791 f 728/724/691 726/722/689 826/824/791 f 825/823/790 826/824/791 726/722/689 f 588/579/546 591/587/554 828/826/793 f 596/586/553 822/821/788 828/826/793 f 716/712/679 730/726/693 828/826/793 f 827/825/792 828/826/793 730/726/693 f 829/827/794 830/828/795 831/829/796 f 834/830/797 831/829/796 830/828/795 f 733/729/696 831/829/796 834/830/797 f 732/746/713 832/831/798 831/829/796 f 835/832/799 836/833/800 834/830/797 f 837/834/801 838/835/802 836/833/800 f 738/734/701 836/833/800 838/835/802 f 736/732/699 834/830/797 836/833/800 f 839/836/803 840/837/804 841/838/805 f 843/839/806 844/840/807 841/838/805 f 845/841/808 846/842/809 841/838/805 f 847/843/810 842/844/811 841/838/805 f 848/845/812 849/846/813 850/847/814 f 832/831/798 850/847/814 849/846/813 f 747/744/711 850/847/814 832/831/798 f 851/848/815 850/847/814 747/744/711 f 847/843/810 846/842/809 852/849/816 f 845/841/808 854/850/817 852/849/816 f 742/739/706 852/849/816 854/850/817 f 853/851/818 852/849/816 742/739/706 f 844/840/807 855/852/819 854/850/817 f 843/839/806 856/853/820 855/852/819 f 851/848/815 855/852/819 856/853/820 f 744/854/821 854/850/817 855/852/819 f 843/839/806 840/837/804 857/855/822 f 839/836/803 858/856/823 857/855/822 f 859/857/824 857/855/822 858/856/823 f 848/845/812 856/853/820 857/855/822 f 839/836/803 842/844/811 860/858/825 f 853/851/818 860/858/825 842/844/811 f 740/738/705 838/835/802 860/858/825 f 858/856/823 860/858/825 838/835/802 f 861/859/826 862/860/827 863/861/828 f 865/862/829 866/863/830 863/861/828 f 227/224/831 863/861/828 866/863/830 f 864/864/832 863/861/828 227/224/831 f 867/865/833 868/866/834 866/863/830 f 870/867/835 868/866/834 867/865/833 f 193/189/166 868/866/834 870/867/835 f 866/863/830 868/866/834 193/189/166 f 871/868/836 872/869/837 870/867/835 f 873/870/838 874/871/839 872/869/837 f 746/743/710 872/869/837 874/871/839 f 870/867/835 872/869/837 746/743/710 f 875/872/840 876/873/841 874/871/839 f 864/864/832 876/873/841 875/872/840 f 8/5/5 876/873/841 864/864/832 f 7/737/704 874/871/839 876/873/841 f 41/32/31 40/31/30 877/874/842 f 39/30/29 879/875/843 877/874/842 f 862/860/827 877/874/842 879/875/843 f 878/876/844 877/874/842 862/860/827 f 39/30/29 38/29/28 880/877/845 f 881/878/846 880/877/845 38/29/28 f 867/865/833 880/877/845 881/878/846 f 865/862/829 879/875/843 880/877/845 f 883/879/847 884/880/848 885/881/849 f 887/882/850 884/880/848 883/879/847 f 889/883/851 884/880/848 887/882/850 f 885/881/849 884/880/848 889/883/851 f 36/33/32 891/884/852 892/885/853 f 41/32/31 878/876/844 891/884/852 f 861/859/826 875/872/840 891/884/852 f 892/885/853 891/884/852 875/872/840 f 894/886/854 895/887/855 896/888/856 f 897/889/857 898/890/858 895/887/855 f 871/868/836 895/887/855 898/890/858 f 869/891/859 896/888/856 895/887/855 f 899/892/860 900/893/861 898/890/858 f 902/894/862 900/893/861 899/892/860 f 33/26/25 892/885/853 900/893/861 f 898/890/858 900/893/861 892/885/853 f 901/895/863 903/896/864 904/897/865 f 906/898/866 904/897/865 903/896/864 f 37/899/867 34/27/26 904/897/865 f 902/894/862 904/897/865 34/27/26 f 907/900/868 908/901/869 906/898/866 f 893/902/870 896/888/856 908/901/869 f 881/878/846 908/901/869 896/888/856 f 37/899/867 906/898/866 908/901/869 f 910/903/871 911/904/872 912/905/873 f 913/906/874 914/907/875 911/904/872 f 894/886/854 911/904/872 914/907/875 f 912/905/873 911/904/872 894/886/854 f 915/908/876 916/909/877 914/907/875 f 917/910/878 918/911/879 916/909/877 f 899/892/860 916/909/877 918/911/879 f 897/889/857 914/907/875 916/909/877 f 917/910/878 919/912/880 920/913/881 f 922/914/882 920/913/881 919/912/880 f 903/896/864 920/913/881 922/914/882 f 918/911/879 920/913/881 903/896/864 f 921/915/883 923/916/884 924/917/885 f 912/905/873 924/917/885 923/916/884 f 893/902/870 907/900/868 924/917/885 f 922/914/882 924/917/885 907/900/868 f 926/918/886 927/919/887 928/920/888 f 929/921/889 930/922/890 927/919/887 f 910/903/871 927/919/887 930/922/890 f 909/923/891 928/920/888 927/919/887 f 931/924/892 932/925/893 930/922/890 f 934/926/894 932/925/893 931/924/892 f 917/910/878 915/908/876 932/925/893 f 913/906/874 930/922/890 932/925/893 f 933/927/895 935/928/896 936/929/897 f 938/930/898 936/929/897 935/928/896 f 921/915/883 919/912/880 936/929/897 f 934/926/894 936/929/897 919/912/880 f 937/931/899 939/932/900 940/933/901 f 925/934/902 928/920/888 940/933/901 f 923/916/884 940/933/901 928/920/888 f 938/930/898 940/933/901 923/916/884 f 942/935/903 943/936/904 944/937/905 f 945/938/906 946/939/907 943/936/904 f 256/263/229 943/936/904 946/939/907 f 255/279/245 944/937/905 943/936/904 f 947/940/908 948/941/909 946/939/907 f 950/942/910 948/941/909 947/940/908 f 263/271/237 261/268/234 948/941/909 f 259/266/232 946/939/907 948/941/909 f 949/943/911 951/944/912 952/945/913 f 954/946/914 952/945/913 951/944/912 f 267/276/242 265/272/238 952/945/913 f 950/942/910 952/945/913 265/272/238 f 953/947/915 955/948/916 956/949/917 f 941/950/918 944/937/905 956/949/917 f 269/277/243 956/949/917 944/937/905 f 954/946/914 956/949/917 269/277/243 f 178/951/919 177/175/152 957/952/920 f 176/174/151 959/953/921 957/952/920 f 945/938/906 942/935/903 957/952/920 f 958/954/922 957/952/920 942/935/903 f 176/174/151 175/173/150 960/955/923 f 174/172/149 961/956/924 960/955/923 f 947/940/908 960/955/923 961/956/924 f 959/953/921 960/955/923 947/940/908 f 174/172/149 171/169/146 962/957/925 f 170/958/926 963/959/927 962/957/925 f 951/944/912 962/957/925 963/959/927 f 949/943/911 961/956/924 962/957/925 f 170/958/926 173/171/148 964/960/928 f 178/951/919 958/954/922 964/960/928 f 941/950/918 955/948/916 964/960/928 f 953/947/915 963/959/927 964/960/928 f 608/600/567 607/599/566 965/961/929 f 606/598/565 967/962/930 965/961/929 f 448/445/411 445/441/407 965/961/929 f 966/963/931 965/961/929 445/441/407 f 606/598/565 605/597/564 968/964/932 f 604/596/563 969/965/933 968/964/932 f 450/446/412 968/964/932 969/965/933 f 967/962/930 968/964/932 450/446/412 f 601/594/561 970/966/934 969/965/933 f 600/593/560 971/967/935 970/966/934 f 454/450/416 970/966/934 971/967/935 f 452/448/414 969/965/933 970/966/934 f 600/593/560 603/601/568 972/968/936 f 608/600/567 966/963/931 972/968/936 f 444/440/406 458/454/420 972/968/936 f 456/453/419 971/967/935 972/968/936 f 973/969/937 974/970/938 975/971/939 f 978/972/940 975/971/939 974/970/938 f 753/750/717 750/747/714 975/971/939 f 976/973/941 975/971/939 750/747/714 f 977/974/942 979/975/943 980/976/944 f 981/977/945 982/978/946 980/976/944 f 755/752/719 980/976/944 982/978/946 f 753/750/717 978/972/940 980/976/944 f 983/979/947 984/980/948 982/978/946 f 985/981/949 986/982/950 984/980/948 f 759/756/723 984/980/948 986/982/950 f 757/754/721 982/978/946 984/980/948 f 987/983/951 988/984/952 986/982/950 f 976/973/941 988/984/952 987/983/951 f 749/985/953 763/761/728 988/984/952 f 761/760/727 986/982/950 988/984/952 f 990/986/954 991/987/955 992/988/956 f 993/989/957 994/990/958 991/987/955 f 995/991/959 996/992/960 991/987/955 f 997/993/961 992/988/956 991/987/955 f 998/994/962 999/995/963 1000/996/964 f 1002/997/965 1003/998/966 1000/996/964 f 979/975/943 1000/996/964 1003/998/966 f 1001/999/967 1000/996/964 979/975/943 f 1002/997/965 1004/1000/968 1005/1001/969 f 1006/1002/970 1007/1003/971 1005/1001/969 f 983/979/947 1005/1001/969 1007/1003/971 f 981/977/945 1003/998/966 1005/1001/969 f 1006/1002/970 1008/1004/972 1009/1005/973 f 1010/1006/974 1011/1007/975 1009/1005/973 f 973/969/937 987/983/951 1009/1005/973 f 985/981/949 1007/1003/971 1009/1005/973 f 347/1008/976 378/376/342 1012/1009/977 f 336/1010/978 334/339/305 1012/1009/977 f 332/336/302 329/334/300 1012/1009/977 f 328/333/299 348/348/314 1012/1009/977 f 1013/1011/979 1014/1012/980 184/179/156 f 1015/1013/981 1016/1014/982 1014/1012/980 f 405/400/366 1014/1012/980 1016/1014/982 f 185/180/157 184/179/156 1014/1012/980 f 1017/1015/983 1018/1016/984 1016/1014/982 f 1019/1017/985 1020/1018/986 1018/1016/984 f 411/406/372 1018/1016/984 1020/1018/986 f 408/403/369 1016/1014/982 1018/1016/984 f 1019/1017/985 1021/1019/987 1022/1020/988 f 182/182/159 1022/1020/988 1021/1019/987 f 187/414/380 415/410/376 1022/1020/988 f 1020/1018/986 1022/1020/988 415/410/376 f 1023/1021/989 1024/1022/990 1025/1023/991 f 1028/1024/992 1025/1023/991 1024/1022/990 f 1015/1013/981 1013/1011/979 1025/1023/991 f 1026/1025/993 1025/1023/991 1013/1011/979 f 1027/1026/994 1029/1027/995 1030/1028/996 f 1031/1029/997 1032/1030/998 1030/1028/996 f 1017/1015/983 1030/1028/996 1032/1030/998 f 1028/1024/992 1030/1028/996 1017/1015/983 f 1033/1031/999 1034/1032/1000 1032/1030/998 f 1035/1033/1001 1036/1034/1002 1034/1032/1000 f 1021/1019/987 1034/1032/1000 1036/1034/1002 f 1019/1017/985 1032/1030/998 1034/1032/1000 f 1037/1035/1003 1038/1036/1004 1036/1034/1002 f 1026/1025/993 1038/1036/1004 1037/1035/1003 f 183/1037/1005 180/177/154 1038/1036/1004 f 179/176/153 1036/1034/1002 1038/1036/1004 f 1040/1038/1006 1041/1039/1007 1042/1040/1008 f 1043/1041/1009 1044/1042/1010 1041/1039/1007 f 1027/1026/994 1024/1022/990 1041/1039/1007 f 1042/1040/1008 1041/1039/1007 1024/1022/990 f 1045/1043/1011 1046/1044/1012 1044/1042/1010 f 1048/1045/1013 1046/1044/1012 1045/1043/1011 f 1029/1027/995 1046/1044/1012 1048/1045/1013 f 1044/1042/1010 1046/1044/1012 1029/1027/995 f 1047/1046/1014 1049/1047/1015 1050/1048/1016 f 1052/1049/1017 1050/1048/1016 1049/1047/1015 f 1033/1031/999 1050/1048/1016 1052/1049/1017 f 1031/1029/997 1048/1045/1013 1050/1048/1016 f 1051/1050/1018 1053/1051/1019 1054/1052/1020 f 1039/1053/1021 1042/1040/1008 1054/1052/1020 f 1023/1021/989 1037/1035/1003 1054/1052/1020 f 1035/1033/1001 1052/1049/1017 1054/1052/1020 f 50/1054/1022 49/38/37 1055/1055/1023 f 48/1056/1024 1057/1057/1025 1055/1055/1023 f 1040/1038/1006 1055/1055/1023 1057/1057/1025 f 1039/1053/1021 1056/1058/1026 1055/1055/1023 f 48/1056/1024 47/37/36 1058/1059/1027 f 46/1060/1028 1059/1061/1029 1058/1059/1027 f 1047/1046/1014 1045/1043/1011 1058/1059/1027 f 1043/1041/1009 1057/1057/1025 1058/1059/1027 f 46/1060/1028 43/34/33 1060/1062/1030 f 42/1063/1031 1061/1064/1032 1060/1062/1030 f 1051/1050/1018 1049/1047/1015 1060/1062/1030 f 1059/1061/1029 1060/1062/1030 1049/1047/1015 f 42/1063/1031 45/36/35 1062/1065/1033 f 50/1054/1022 1056/1058/1026 1062/1065/1033 f 1053/1051/1019 1062/1065/1033 1056/1058/1026 f 1061/1064/1032 1062/1065/1033 1053/1051/1019 f 401/1066/1034 400/394/360 1063/1067/1035 f 399/1068/1036 1065/1069/1037 1063/1067/1035 f 493/485/452 1063/1067/1035 1065/1069/1037 f 492/501/468 1064/1070/1038 1063/1067/1035 f 399/1068/1036 398/393/359 1066/1071/1039 f 397/1072/1040 1067/1073/1041 1066/1071/1039 f 500/494/461 498/490/457 1066/1071/1039 f 496/488/455 1065/1069/1037 1066/1071/1039 f 397/1072/1040 394/390/356 1068/1074/1042 f 393/1075/1043 1069/1076/1044 1068/1074/1042 f 504/498/465 502/495/462 1068/1074/1042 f 1067/1073/1041 1068/1074/1042 502/495/462 f 393/1075/1043 396/392/358 1070/1077/1045 f 401/1066/1034 1064/1070/1038 1070/1077/1045 f 506/499/466 1070/1077/1045 1064/1070/1038 f 1069/1076/1044 1070/1077/1045 506/499/466 f 1071/1078/1046 1072/1079/1047 1073/1080/1048 f 1076/1081/1049 1073/1080/1048 1072/1079/1047 f 346/469/436 345/347/313 1073/1080/1048 f 1074/1082/1050 1073/1080/1048 345/347/313 f 1075/1083/1051 1077/1084/1052 1078/1085/1053 f 1079/1086/1054 1080/1087/1055 1078/1085/1053 f 381/1088/1056 475/468/435 1078/1085/1053 f 1076/1081/1049 1078/1085/1053 475/468/435 f 1081/1089/1057 1082/1090/1058 1080/1087/1055 f 1083/1091/1059 1084/1092/1060 1082/1090/1058 f 382/379/345 1082/1090/1058 1084/1092/1060 f 381/1088/1056 1080/1087/1055 1082/1090/1058 f 1083/1091/1059 1085/1093/1061 1086/1094/1062 f 1074/1082/1050 1086/1094/1062 1085/1093/1061 f 464/460/426 1086/1094/1062 1074/1082/1050 f 385/382/348 1084/1092/1060 1086/1094/1062 f 1087/1095/1063 1088/1096/1064 1089/1097/1065 f 1092/1098/1066 1089/1097/1065 1088/1096/1064 f 1075/1083/1051 1072/1079/1047 1089/1097/1065 f 1090/1099/1067 1089/1097/1065 1072/1079/1047 f 1093/1100/1068 1094/1101/1069 1092/1098/1066 f 1095/1102/1070 1096/1103/1071 1094/1101/1069 f 1077/1084/1052 1094/1101/1069 1096/1103/1071 f 1092/1098/1066 1094/1101/1069 1077/1084/1052 f 1097/1104/1072 1098/1105/1073 1096/1103/1071 f 1099/1106/1074 1100/1107/1075 1098/1105/1073 f 1081/1089/1057 1098/1105/1073 1100/1107/1075 f 1079/1086/1054 1096/1103/1071 1098/1105/1073 f 1099/1106/1074 1101/1108/1076 1102/1109/1077 f 1090/1099/1067 1102/1109/1077 1101/1108/1076 f 1071/1078/1046 1085/1093/1061 1102/1109/1077 f 1100/1107/1075 1102/1109/1077 1085/1093/1061 f 1104/1110/1078 1105/1111/1079 1106/1112/1080 f 1107/1113/1081 1108/1114/1082 1105/1111/1079 f 1088/1096/1064 1105/1111/1079 1108/1114/1082 f 1087/1095/1063 1106/1112/1080 1105/1111/1079 f 1109/1115/1083 1110/1116/1084 1108/1114/1082 f 1112/1117/1085 1110/1116/1084 1109/1115/1083 f 1095/1102/1070 1093/1100/1068 1110/1116/1084 f 1091/1118/1086 1108/1114/1082 1110/1116/1084 f 1111/1119/1087 1113/1120/1088 1114/1121/1089 f 1116/1122/1090 1114/1121/1089 1113/1120/1088 f 1099/1106/1074 1097/1104/1072 1114/1121/1089 f 1112/1117/1085 1114/1121/1089 1097/1104/1072 f 1115/1123/1091 1117/1124/1092 1118/1125/1093 f 1103/1126/1094 1106/1112/1080 1118/1125/1093 f 1101/1108/1076 1118/1125/1093 1106/1112/1080 f 1116/1122/1090 1118/1125/1093 1101/1108/1076 f 1119/1127/1095 1120/1128/1096 1121/1129/1097 f 1124/1130/1098 1121/1129/1097 1120/1128/1096 f 1107/1113/1081 1104/1110/1078 1121/1129/1097 f 1103/1126/1094 1122/1131/1099 1121/1129/1097 f 1123/1132/1100 1125/1133/1101 1126/1134/1102 f 1128/1135/1103 1126/1134/1102 1125/1133/1101 f 1111/1119/1087 1109/1115/1083 1126/1134/1102 f 1124/1130/1098 1126/1134/1102 1109/1115/1083 f 1129/1136/1104 1130/1137/1105 1128/1135/1103 f 1131/464/1106 1132/1138/1107 1130/1137/1105 f 1113/1120/1088 1130/1137/1105 1132/1138/1107 f 1111/1119/1087 1128/1135/1103 1130/1137/1105 f 1133/1139/1108 1134/1140/1109 1132/1138/1107 f 1119/1127/1095 1122/1131/1099 1134/1140/1109 f 1117/1124/1092 1134/1140/1109 1122/1131/1099 f 1115/1123/1091 1132/1138/1107 1134/1140/1109 f 473/1141/1110 472/466/433 1135/1142/1111 f 471/1143/1112 1137/1144/1113 1135/1142/1111 f 1120/1128/1096 1135/1142/1111 1137/1144/1113 f 1119/1127/1095 1136/1145/1114 1135/1142/1111 f 471/1143/1112 470/465/431 1138/1146/1115 f 469/1147/1116 1139/1148/1117 1138/1146/1115 f 1127/1149/1118 1125/1133/1101 1138/1146/1115 f 1137/1144/1113 1138/1146/1115 1125/1133/1101 f 469/1147/1116 466/462/428 1140/1150/1119 f 465/1151/1120 1141/1152/1121 1140/1150/1119 f 1131/464/1106 1129/1136/1104 1140/1150/1119 f 1139/1148/1117 1140/1150/1119 1129/1136/1104 f 465/1151/1120 468/464/430 1142/1153/1122 f 473/1141/1110 1136/1145/1114 1142/1153/1122 f 1133/1139/1108 1142/1153/1122 1136/1145/1114 f 1131/464/1106 1141/1152/1121 1142/1153/1122 f 1143/1154/1123 1144/1155/1124 1145/1156/1125 f 1148/1157/1126 1145/1156/1125 1144/1155/1124 f 833/1158/1127 830/828/795 1145/1156/1125 f 1146/1159/1128 1145/1156/1125 830/828/795 f 1147/1160/1129 1149/1161/1130 1150/1162/1131 f 1151/1163/1132 1152/1164/1133 1150/1162/1131 f 835/832/799 1150/1162/1131 1152/1164/1133 f 833/1158/1127 1148/1157/1126 1150/1162/1131 f 1153/1165/1134 1154/1166/1135 1152/1164/1133 f 1155/1167/1136 1156/1168/1137 1154/1166/1135 f 859/857/824 1154/1166/1135 1156/1168/1137 f 1152/1164/1133 1154/1166/1135 859/857/824 f 1157/1169/1138 1158/1170/1139 1156/1168/1137 f 1146/1159/1128 1158/1170/1139 1157/1169/1138 f 829/827/794 849/846/813 1158/1170/1139 f 1156/1168/1137 1158/1170/1139 849/846/813 f 1159/1171/1140 1160/1172/1141 1161/1173/1142 f 1162/1174/1143 1163/1175/1144 1160/1172/1141 f 1144/1155/1124 1160/1172/1141 1163/1175/1144 f 1143/1154/1123 1161/1173/1142 1160/1172/1141 f 1164/1176/1145 1165/1177/1146 1163/1175/1144 f 1167/1178/1147 1165/1177/1146 1164/1176/1145 f 1151/1163/1132 1149/1161/1130 1165/1177/1146 f 1147/1160/1129 1163/1175/1144 1165/1177/1146 f 1166/1179/1148 1168/1180/1149 1169/1181/1150 f 1170/1182/1151 1169/1181/1150 1168/1180/1149 f 1155/1167/1136 1153/1165/1134 1169/1181/1150 f 1167/1178/1147 1169/1181/1150 1153/1165/1134 f 306/1183/1152 305/311/277 1171/1184/1153 f 304/1185/1154 1161/1173/1142 1171/1184/1153 f 1157/1169/1138 1171/1184/1153 1161/1173/1142 f 1170/1182/1151 1171/1184/1153 1157/1169/1138 f 355/353/319 1172/1186/1155 303/310/276 f 354/359/325 1173/1187/1156 1172/1186/1155 f 1159/1171/1140 1172/1186/1155 1173/1187/1156 f 304/1185/1154 303/310/276 1172/1186/1155 f 359/358/324 1174/1188/1157 1173/1187/1156 f 1175/1189/1158 1174/1188/1157 359/358/324 f 1166/1179/1148 1164/1176/1145 1174/1188/1157 f 1162/1174/1143 1173/1187/1156 1174/1188/1157 f 358/357/323 362/362/328 1176/1190/1159 f 301/312/278 1176/1190/1159 362/362/328 f 306/1183/1152 1168/1180/1149 1176/1190/1159 f 1175/1189/1158 1176/1190/1159 1168/1180/1149 f 1006/1002/970 1004/1000/968 1177/1191/1160 f 1002/997/965 999/995/963 1177/1191/1160 f 998/994/962 1178/1192/1161 1177/1191/1160 f 1008/1004/972 1177/1191/1160 1178/1192/1161 f 1179/1193/1162 1180/1194/1163 1181/1195/1164 f 1184/1196/1165 1181/1195/1164 1180/1194/1163 f 977/974/942 974/970/938 1181/1195/1164 f 1182/1197/1166 1181/1195/1164 974/970/938 f 1185/1198/1167 1186/1199/1168 1184/1196/1165 f 1188/1200/1169 1186/1199/1168 1185/1198/1167 f 1001/999/967 1186/1199/1168 1188/1200/1169 f 1184/1196/1165 1186/1199/1168 1001/999/967 f 1187/1201/1170 1189/1202/1171 1190/1203/1172 f 1192/1204/1173 1190/1203/1172 1189/1202/1171 f 1010/1006/974 1178/1192/1161 1190/1203/1172 f 1188/1200/1169 1190/1203/1172 1178/1192/1161 f 1191/1205/1174 1193/1206/1175 1194/1207/1176 f 1182/1197/1166 1194/1207/1176 1193/1206/1175 f 1011/1007/975 1194/1207/1176 1182/1197/1166 f 1192/1204/1173 1194/1207/1176 1011/1007/975 f 997/993/961 996/992/960 1195/1208/1177 f 995/991/959 1197/1209/1178 1195/1208/1177 f 1183/1210/1179 1180/1194/1163 1195/1208/1177 f 1196/1211/1180 1195/1208/1177 1180/1194/1163 f 995/991/959 994/990/958 1198/1212/1181 f 993/989/957 1199/1213/1182 1198/1212/1181 f 1185/1198/1167 1198/1212/1181 1199/1213/1182 f 1183/1210/1179 1197/1209/1178 1198/1212/1181 f 1200/1214/1183 1201/1215/1184 1202/1216/1185 f 1204/1217/1186 1205/1218/1187 1202/1216/1185 f 1206/1219/1188 1207/1220/1189 1202/1216/1185 f 1208/1221/1190 1203/1222/1191 1202/1216/1185 f 992/988/956 1209/1223/1192 1210/1224/1193 f 997/993/961 1196/1211/1180 1209/1223/1192 f 1193/1206/1175 1209/1223/1192 1196/1211/1180 f 1210/1224/1193 1209/1223/1192 1193/1206/1175 f 1211/1225/1194 1212/1226/1195 1213/1227/1196 f 1216/1228/1197 1213/1227/1196 1212/1226/1195 f 1191/1205/1174 1189/1202/1171 1213/1227/1196 f 1214/1229/1198 1213/1227/1196 1189/1202/1171 f 1215/1230/1199 1217/1231/1200 1218/1232/1201 f 1220/1233/1202 1218/1232/1201 1217/1231/1200 f 989/1234/1203 1210/1224/1193 1218/1232/1201 f 1216/1228/1197 1218/1232/1201 1210/1224/1193 f 1221/1235/1204 1222/1236/1205 1220/1233/1202 f 1223/1237/1206 1224/1238/1207 1222/1236/1205 f 990/986/954 1222/1236/1205 1224/1238/1207 f 989/1234/1203 1220/1233/1202 1222/1236/1205 f 1225/1239/1208 1226/1240/1209 1224/1238/1207 f 1211/1225/1194 1214/1229/1198 1226/1240/1209 f 1199/1213/1182 1226/1240/1209 1214/1229/1198 f 993/989/957 1224/1238/1207 1226/1240/1209 f 1227/1241/1210 1228/1242/1211 1229/1243/1212 f 1232/1244/1213 1229/1243/1212 1228/1242/1211 f 1215/1230/1199 1212/1226/1195 1229/1243/1212 f 1230/1245/1214 1229/1243/1212 1212/1226/1195 f 1231/1246/1215 1233/1247/1216 1234/1248/1217 f 1235/1249/1218 1236/1250/1219 1234/1248/1217 f 1217/1231/1200 1234/1248/1217 1236/1250/1219 f 1232/1244/1213 1234/1248/1217 1217/1231/1200 f 1237/1251/1220 1238/1252/1221 1236/1250/1219 f 1239/1253/1222 1240/1254/1223 1238/1252/1221 f 1221/1235/1204 1238/1252/1221 1240/1254/1223 f 1219/1255/1224 1236/1250/1219 1238/1252/1221 f 1241/1256/1225 1242/1257/1226 1240/1254/1223 f 1227/1241/1210 1230/1245/1214 1242/1257/1226 f 1225/1239/1208 1242/1257/1226 1230/1245/1214 f 1223/1237/1206 1240/1254/1223 1242/1257/1226 f 1243/1258/1227 1244/1259/1228 1245/1260/1229 f 1248/1261/1230 1245/1260/1229 1244/1259/1228 f 1231/1246/1215 1228/1242/1211 1245/1260/1229 f 1246/1262/1231 1245/1260/1229 1228/1242/1211 f 1247/1263/1232 1249/1264/1233 1250/1265/1234 f 1252/1266/1235 1250/1265/1234 1249/1264/1233 f 1235/1249/1218 1233/1247/1216 1250/1265/1234 f 1248/1261/1230 1250/1265/1234 1233/1247/1216 f 1253/1267/1236 1254/1268/1237 1252/1266/1235 f 1255/1269/1238 1256/1270/1239 1254/1268/1237 f 1237/1251/1220 1254/1268/1237 1256/1270/1239 f 1235/1249/1218 1252/1266/1235 1254/1268/1237 f 1257/1271/1240 1258/1272/1241 1256/1270/1239 f 1243/1258/1227 1246/1262/1231 1258/1272/1241 f 1241/1256/1225 1258/1272/1241 1246/1262/1231 f 1239/1253/1222 1256/1270/1239 1258/1272/1241 f 1208/1221/1190 1207/1220/1189 1259/1273/1242 f 1206/1219/1188 1261/1274/1243 1259/1273/1242 f 1247/1263/1232 1244/1259/1228 1259/1273/1242 f 1260/1275/1244 1259/1273/1242 1244/1259/1228 f 1206/1219/1188 1205/1218/1187 1262/1276/1245 f 1204/1217/1186 1263/1277/1246 1262/1276/1245 f 1251/1278/1247 1249/1264/1233 1262/1276/1245 f 1261/1274/1243 1262/1276/1245 1249/1264/1233 f 1204/1217/1186 1201/1215/1184 1264/1279/1248 f 1200/1214/1183 1265/1280/1249 1264/1279/1248 f 1253/1267/1236 1264/1279/1248 1265/1280/1249 f 1251/1278/1247 1263/1277/1246 1264/1279/1248 f 1200/1214/1183 1203/1222/1191 1266/1281/1250 f 1208/1221/1190 1260/1275/1244 1266/1281/1250 f 1257/1271/1240 1266/1281/1250 1260/1275/1244 f 1255/1269/1238 1265/1280/1249 1266/1281/1250 f 890/1282/1251 889/883/851 1267/1283/1252 f 888/1284/1253 1269/1285/1254 1267/1283/1252 f 926/918/886 1267/1283/1252 1269/1285/1254 f 925/934/902 1268/1286/1255 1267/1283/1252 f 888/1284/1253 887/882/850 1270/1287/1256 f 886/1288/1257 1271/1289/1258 1270/1287/1256 f 931/924/892 1270/1287/1256 1271/1289/1258 f 929/921/889 1269/1285/1254 1270/1287/1256 f 886/1288/1257 883/879/847 1272/1290/1259 f 882/1291/1260 1273/1292/1261 1272/1290/1259 f 937/931/899 935/928/896 1272/1290/1259 f 1271/1289/1258 1272/1290/1259 935/928/896 f 882/1291/1260 885/881/849 1274/1293/1262 f 890/1282/1251 1268/1286/1255 1274/1293/1262 f 939/932/900 1274/1293/1262 1268/1286/1255 f 1273/1292/1261 1274/1293/1262 939/932/900 f 1/74/73 2/1/1 4/3/3 f 5/73/72 6/4/4 2/1/1 f 7/737/704 8/5/5 6/4/4 f 8/5/5 9/6/6 3/2/2 f 11/1294/206 4/3/3 10/7/7 f 12/79/78 13/8/8 15/10/10 f 16/83/82 17/11/11 13/8/8 f 18/39/38 19/12/12 17/11/11 f 19/12/12 20/13/13 14/9/9 f 22/416/382 15/10/10 21/14/14 f 5/73/72 23/15/15 6/4/4 f 12/79/78 15/10/10 23/15/15 f 22/416/382 25/17/17 15/10/10 f 25/17/17 26/18/18 24/16/16 f 7/737/704 6/4/4 27/19/19 f 16/83/82 28/20/20 17/11/11 f 1/88/73 4/22/3 28/20/20 f 11/232/206 30/23/22 4/22/3 f 30/23/22 31/24/23 29/21/21 f 18/39/38 17/11/11 32/25/24 f 36/33/32 33/26/25 35/28/27 f 37/899/867 38/29/28 34/27/26 f 38/29/28 39/30/29 35/28/27 f 40/31/30 41/32/31 35/28/27 f 42/1063/1031 43/34/33 45/36/35 f 46/1060/1028 47/37/36 43/34/33 f 48/1056/1024 49/38/37 47/37/36 f 50/1054/1022 45/36/35 49/38/37 f 19/12/12 18/39/38 52/41/40 f 53/1295/1263 54/42/41 51/40/39 f 55/144/122 56/43/42 54/42/41 f 20/13/13 19/12/12 56/43/42 f 60/51/50 57/44/43 59/46/45 f 58/45/44 61/47/46 59/46/45 f 63/555/522 64/49/48 62/48/47 f 64/49/48 65/50/49 59/46/45 f 69/60/59 66/52/51 68/54/53 f 67/53/52 70/55/54 68/54/53 f 71/56/55 72/57/56 68/54/53 f 73/58/57 74/59/58 68/54/53 f 78/68/67 75/61/60 77/63/62 f 76/62/61 79/64/63 77/63/62 f 81/210/187 82/66/65 80/65/64 f 82/66/65 83/67/66 77/63/62 f 87/75/74 84/69/68 86/71/70 f 88/99/91 89/72/71 85/70/69 f 89/72/71 5/73/72 86/71/70 f 2/1/1 1/74/73 86/71/70 f 88/99/91 90/76/75 89/72/71 f 92/1296/1264 93/78/77 90/76/75 f 93/78/77 12/79/78 91/77/76 f 23/15/15 5/73/72 91/77/76 f 92/1296/1264 94/80/79 93/78/77 f 96/84/83 97/82/81 94/80/79 f 97/82/81 16/83/82 95/81/80 f 13/8/8 12/79/78 95/81/80 f 97/82/81 96/84/83 99/86/85 f 84/107/68 87/87/74 98/85/84 f 87/87/74 1/88/73 99/86/85 f 28/20/20 16/83/82 99/86/85 f 103/94/88 100/89/68 102/91/87 f 101/90/86 104/92/71 102/91/87 f 88/99/91 85/70/69 105/93/71 f 84/69/68 103/94/88 85/70/69 f 104/92/71 106/95/89 105/93/71 f 106/95/89 108/97/90 107/96/75 f 92/1296/1264 90/76/75 109/98/90 f 90/76/75 88/99/91 107/96/75 f 108/97/90 110/100/79 109/98/90 f 112/103/93 113/102/92 110/100/79 f 113/102/92 96/84/83 111/101/79 f 92/1296/1264 109/98/90 94/80/79 f 113/102/92 112/103/93 115/105/95 f 100/1297/68 103/106/88 114/104/94 f 103/106/88 84/107/68 115/105/95 f 96/84/83 113/102/92 98/85/84 f 119/112/88 116/108/96 118/110/98 f 120/1298/71 121/111/99 117/109/97 f 104/92/71 101/90/86 121/111/99 f 100/89/68 119/112/88 101/90/86 f 120/1298/71 122/113/100 121/111/99 f 122/113/100 124/115/90 123/114/75 f 108/97/90 106/95/89 125/116/101 f 104/92/71 121/111/99 106/95/89 f 124/115/90 126/117/102 125/116/101 f 128/120/93 129/119/104 126/117/102 f 129/119/104 112/103/93 127/118/103 f 110/100/79 108/97/90 127/118/103 f 129/119/104 128/120/93 131/122/106 f 116/139/96 119/123/88 130/121/105 f 100/1297/68 114/104/94 119/123/88 f 112/103/93 129/119/104 114/104/94 f 133/138/118 74/59/58 132/124/107 f 73/58/57 72/57/56 132/124/107 f 120/1298/71 117/109/97 134/127/109 f 116/108/96 133/128/110 117/109/97 f 134/125/108 72/57/56 135/129/111 f 71/56/55 70/55/54 135/129/111 f 124/115/90 122/113/100 136/132/113 f 120/1298/71 134/127/109 122/113/100 f 136/130/112 70/55/54 137/133/114 f 67/53/52 66/52/51 137/133/114 f 128/120/93 126/117/102 138/136/116 f 124/115/90 136/132/113 126/117/102 f 138/134/115 66/52/51 139/137/117 f 69/60/59 74/59/58 139/137/117 f 133/1299/110 116/139/96 139/140/94 f 128/120/93 138/136/116 130/121/105 f 76/62/61 75/61/60 141/142/120 f 142/759/726 143/143/121 140/141/119 f 143/143/121 55/144/122 141/142/120 f 79/64/63 76/62/61 144/145/123 f 148/152/130 145/146/124 147/148/126 f 149/742/709 150/149/127 146/147/125 f 150/149/127 151/150/128 147/148/126 f 152/151/129 83/67/66 147/148/126 f 156/217/138 153/153/131 155/155/133 f 157/213/190 158/156/134 154/154/132 f 158/243/134 159/157/135 155/159/133 f 160/158/136 161/160/137 155/159/133 f 165/168/145 162/162/139 164/164/141 f 53/1295/1263 166/165/142 163/163/140 f 166/165/142 167/166/143 164/164/141 f 169/184/161 165/168/145 168/167/144 f 170/958/926 171/169/146 173/171/148 f 171/169/146 174/172/149 172/170/147 f 175/173/150 176/174/151 172/170/147 f 178/951/919 173/171/148 177/175/152 f 182/182/159 179/176/153 181/178/155 f 183/1037/1005 184/179/156 180/177/154 f 184/179/156 185/180/157 181/178/155 f 187/414/380 182/182/159 186/181/158 f 189/186/163 162/162/139 188/183/160 f 165/168/145 169/184/161 188/183/160 f 81/210/187 80/65/64 190/185/162 f 80/65/64 79/64/63 188/183/160 f 53/1295/1263 163/163/140 54/42/41 f 163/163/140 162/162/139 191/187/164 f 189/186/163 79/64/63 191/187/164 f 55/144/122 54/42/41 144/145/123 f 149/742/709 146/147/125 193/189/166 f 146/147/125 145/146/124 192/188/165 f 194/190/167 161/160/137 192/188/165 f 160/158/136 159/157/135 192/188/165 f 198/199/176 195/191/168 197/193/170 f 196/192/169 199/194/171 197/193/170 f 200/195/172 201/196/173 197/193/170 f 202/197/174 203/198/175 197/193/170 f 207/207/184 204/200/177 206/202/179 f 205/201/178 208/203/180 206/202/179 f 210/615/582 211/205/182 209/204/181 f 211/205/182 212/206/183 206/202/179 f 161/160/137 194/190/167 214/209/186 f 194/190/167 145/146/124 213/208/185 f 83/67/66 82/66/65 148/152/130 f 82/66/65 81/210/187 213/208/185 f 142/759/726 140/141/119 216/212/189 f 140/141/119 75/61/60 215/211/188 f 83/67/66 152/151/129 78/68/67 f 152/151/129 151/150/128 215/211/188 f 218/216/193 157/213/190 217/214/191 f 153/153/131 219/215/192 154/154/132 f 219/215/192 169/184/161 217/214/191 f 167/166/143 218/216/193 168/167/144 f 219/215/192 153/153/131 220/218/194 f 161/160/137 214/209/186 156/161/138 f 214/209/186 81/210/187 220/219/194 f 169/184/161 219/215/192 190/185/162 f 224/225/200 221/220/195 223/222/197 f 225/226/201 226/223/198 222/221/196 f 226/223/198 9/6/6 223/222/197 f 159/157/135 224/225/200 227/224/199 f 226/223/198 225/226/201 229/228/203 f 228/254/202 230/229/204 229/231/203 f 231/230/205 11/232/206 229/231/203 f 9/6/6 226/223/198 10/7/7 f 230/229/204 232/234/207 231/230/205 f 232/234/207 234/236/209 233/235/208 f 157/213/190 236/238/211 235/237/210 f 236/238/211 11/232/206 233/235/208 f 234/236/209 237/239/212 235/237/210 f 221/220/195 224/225/200 237/242/212 f 159/157/135 158/243/134 224/225/200 f 157/213/190 235/237/210 158/156/134 f 239/261/228 240/244/214 242/246/216 f 240/244/214 243/247/217 241/245/215 f 244/248/218 225/249/201 241/245/215 f 221/262/195 242/246/216 222/250/196 f 244/248/218 243/247/217 246/252/220 f 247/255/222 248/253/221 245/251/219 f 230/229/204 228/254/202 248/253/221 f 225/249/201 244/248/218 228/254/202 f 248/253/221 247/255/222 250/257/224 f 251/275/241 252/258/225 249/256/223 f 234/236/209 232/234/207 252/258/225 f 232/234/207 230/229/204 250/257/224 f 251/275/241 253/259/226 252/258/225 f 253/259/226 239/261/228 254/260/227 f 242/246/216 221/262/195 254/260/227 f 237/239/212 234/236/209 254/260/227 f 255/279/245 256/263/229 258/265/231 f 256/263/229 259/266/232 257/264/230 f 243/247/217 240/244/214 260/267/233 f 240/244/214 239/261/228 257/264/230 f 259/266/232 261/268/234 260/267/233 f 263/271/237 264/270/236 261/268/234 f 264/270/236 247/255/222 262/269/235 f 245/251/219 243/247/217 262/269/235 f 264/270/236 263/271/237 266/273/239 f 267/276/242 268/274/240 265/272/238 f 268/274/240 251/275/241 266/273/239 f 247/255/222 264/270/236 249/256/223 f 268/274/240 267/276/242 270/278/244 f 269/277/243 255/279/245 270/278/244 f 239/261/228 253/259/226 258/265/231 f 251/275/241 268/274/240 253/259/226 f 274/285/251 271/280/246 273/282/248 f 275/286/252 276/283/249 272/281/247 f 276/283/249 167/166/143 273/282/248 f 31/24/23 274/285/251 277/284/250 f 276/283/249 275/286/252 279/288/254 f 278/287/253 280/289/255 279/288/254 f 157/213/190 218/216/193 281/290/256 f 167/166/143 276/283/249 218/216/193 f 280/289/255 282/291/257 281/290/256 f 282/291/257 284/293/259 283/292/258 f 11/232/206 236/238/211 285/294/260 f 236/238/211 157/213/190 283/292/258 f 284/293/259 286/295/261 285/294/260 f 271/280/246 274/285/251 286/295/261 f 31/24/23 30/23/22 274/285/251 f 11/232/206 285/294/260 30/23/22 f 291/301/267 288/297/263 290/299/265 f 292/302/268 293/300/266 289/298/264 f 293/300/266 275/286/252 290/299/265 f 271/280/246 291/301/267 272/281/247 f 293/300/266 292/302/268 295/304/270 f 296/328/294 297/305/271 294/303/269 f 280/289/255 278/287/253 297/305/271 f 275/286/252 293/300/266 278/287/253 f 301/312/278 298/306/272 300/308/274 f 299/307/273 302/309/275 300/308/274 f 304/1185/1154 305/311/277 303/310/276 f 306/1183/1152 301/312/278 305/311/277 f 307/1300/1265 308/313/279 310/315/281 f 288/297/263 291/301/267 308/313/279 f 291/301/267 271/280/246 309/314/280 f 286/295/261 284/293/259 309/314/280 f 311/332/298 312/316/282 314/318/284 f 312/316/282 315/319/285 313/317/283 f 284/293/259 282/291/257 316/320/286 f 282/291/257 280/289/255 313/317/283 f 315/319/285 317/321/287 316/320/286 f 319/324/290 320/323/289 317/321/287 f 307/1300/1265 310/315/281 320/323/289 f 284/293/259 316/320/286 310/315/281 f 320/323/289 319/324/290 322/326/292 f 323/368/334 324/327/293 321/325/291 f 324/327/293 296/328/294 322/326/292 f 307/1300/1265 320/323/289 325/329/295 f 323/368/334 326/330/296 324/327/293 f 326/330/296 311/332/298 327/331/297 f 280/289/255 297/305/271 314/318/284 f 296/328/294 324/327/293 297/305/271 f 331/338/304 328/333/299 330/335/301 f 329/334/300 332/336/302 330/335/301 f 315/319/285 312/316/282 333/337/303 f 312/316/282 311/332/298 330/335/301 f 333/337/303 332/336/302 335/340/306 f 336/1010/978 337/341/307 334/339/305 f 319/324/290 317/321/287 337/341/307 f 317/321/287 315/319/285 335/340/306 f 338/395/361 339/342/308 341/344/310 f 339/342/308 342/345/311 340/343/309 f 344/461/427 345/347/313 343/346/312 f 346/469/436 341/344/310 345/347/313 f 347/1008/976 348/348/314 350/350/316 f 348/348/314 328/333/299 349/349/315 f 331/338/304 311/332/298 349/349/315 f 323/368/334 350/350/316 326/330/296 f 352/354/320 203/198/175 351/351/317 f 202/197/174 201/196/173 351/351/317 f 354/359/325 355/353/319 353/352/318 f 355/353/319 302/309/275 351/351/317 f 353/352/318 201/196/173 356/355/321 f 200/195/172 199/194/171 356/355/321 f 357/356/322 358/357/323 356/355/321 f 359/358/324 354/359/325 356/355/321 f 357/356/322 199/194/171 360/360/326 f 196/192/169 195/191/168 360/360/326 f 361/361/327 298/306/272 360/360/326 f 358/357/323 357/356/322 362/362/328 f 361/361/327 195/191/168 363/363/329 f 198/199/176 203/198/175 363/363/329 f 302/309/275 299/307/273 352/354/320 f 298/306/272 361/361/327 299/307/273 f 364/1301/1266 365/364/330 367/366/332 f 368/1302/1267 369/367/333 365/364/330 f 369/367/333 323/368/334 366/365/331 f 319/324/290 367/366/332 321/325/291 f 368/1302/1267 370/369/335 369/367/333 f 372/372/338 373/371/337 370/369/335 f 347/1008/976 350/350/316 373/371/337 f 323/368/334 369/367/333 350/350/316 f 373/371/337 372/372/338 375/374/340 f 376/398/364 377/375/341 374/373/339 f 336/1010/978 378/376/342 377/375/341 f 347/1008/976 373/371/337 378/376/342 f 376/398/364 379/377/343 377/375/341 f 364/1301/1266 367/366/332 379/377/343 f 319/324/290 337/341/307 367/366/332 f 336/1010/978 377/375/341 337/341/307 f 381/1088/1056 382/379/345 384/381/347 f 382/379/345 385/382/348 383/380/346 f 386/383/349 387/384/350 383/380/346 f 388/385/351 389/386/352 383/380/346 f 344/461/427 343/346/312 391/388/354 f 342/345/311 392/389/355 343/346/312 f 392/389/355 372/372/338 390/387/353 f 368/1302/1267 391/388/354 370/369/335 f 393/1075/1043 394/390/356 396/392/358 f 397/1072/1040 398/393/359 394/390/356 f 399/1068/1036 400/394/360 398/393/359 f 401/1066/1034 396/392/358 400/394/360 f 403/399/365 338/395/361 402/396/362 f 346/469/436 404/397/363 341/344/310 f 364/1301/1266 379/377/343 404/397/363 f 379/377/343 376/398/364 402/396/362 f 185/180/157 405/400/366 407/402/368 f 405/400/366 408/403/369 406/401/367 f 151/150/128 410/405/371 409/404/370 f 410/405/371 26/18/18 406/401/367 f 408/403/369 411/406/372 409/404/370 f 413/409/375 414/408/374 411/406/372 f 142/759/726 216/212/189 414/408/374 f 151/150/128 409/404/370 216/212/189 f 414/408/374 413/409/375 416/411/377 f 187/414/380 417/412/378 415/410/376 f 22/416/382 418/413/379 417/412/378 f 142/759/726 414/408/374 418/413/379 f 417/412/378 187/414/380 419/415/381 f 186/181/158 185/180/157 419/415/381 f 26/18/18 25/17/17 407/402/368 f 25/17/17 22/416/382 419/415/381 f 423/423/389 420/417/383 422/419/385 f 424/424/390 425/420/386 421/418/384 f 425/420/386 426/421/387 422/419/385 f 428/632/599 423/423/389 427/422/388 f 425/420/386 424/424/390 430/426/392 f 429/425/391 431/427/393 430/426/392 f 432/428/394 433/429/395 430/426/392 f 426/421/387 425/420/386 434/430/396 f 431/427/393 435/431/397 432/428/394 f 435/431/397 437/433/399 436/432/398 f 439/439/405 440/435/401 438/434/400 f 440/435/401 433/429/395 436/432/398 f 437/433/399 441/436/402 438/434/400 f 441/436/402 420/417/383 442/437/403 f 428/632/599 443/438/404 423/423/389 f 443/438/404 439/439/405 442/437/403 f 447/444/410 444/440/406 446/442/408 f 448/445/411 449/443/409 445/441/407 f 449/443/409 424/424/390 446/442/408 f 420/417/383 447/444/410 421/418/384 f 449/443/409 448/445/411 451/447/413 f 450/446/412 452/448/414 451/447/413 f 453/449/415 431/427/393 451/447/413 f 424/424/390 449/443/409 429/425/391 f 452/448/414 454/450/416 453/449/415 f 456/453/419 457/452/418 454/450/416 f 437/433/399 435/431/397 457/452/418 f 435/431/397 431/427/393 455/451/417 f 457/452/418 456/453/419 459/455/421 f 444/440/406 447/444/410 458/454/420 f 447/444/410 420/417/383 459/455/421 f 441/436/402 437/433/399 459/455/421 f 461/458/424 389/386/352 460/456/422 f 388/385/351 387/384/350 460/456/422 f 368/1302/1267 365/364/330 462/457/423 f 364/1301/1266 461/458/424 365/364/330 f 462/457/423 387/384/350 463/459/425 f 385/382/348 464/460/426 386/383/349 f 464/460/426 344/461/427 463/459/425 f 368/1302/1267 462/457/423 391/388/354 f 465/1151/1120 466/462/428 468/464/430 f 469/1147/1116 470/465/431 466/462/428 f 471/1143/1112 472/466/433 470/465/431 f 473/1141/1110 468/464/430 472/466/433 f 381/1088/1056 384/381/347 475/468/435 f 384/381/347 389/386/352 474/467/434 f 364/1301/1266 404/397/363 461/458/424 f 404/397/363 346/469/436 474/467/434 f 479/474/441 476/470/437 478/472/439 f 480/475/442 481/473/440 477/471/438 f 481/473/440 376/398/364 478/472/439 f 372/372/338 479/474/441 374/373/339 f 481/473/440 480/475/442 483/477/444 f 484/493/460 485/478/445 482/476/443 f 338/395/361 403/399/365 485/478/445 f 376/398/364 481/473/440 403/399/365 f 484/493/460 486/479/446 485/478/445 f 486/479/446 488/481/448 487/480/447 f 342/345/311 339/342/308 489/482/449 f 339/342/308 338/395/361 487/480/447 f 488/481/448 490/483/450 489/482/449 f 490/483/450 476/470/437 491/484/451 f 372/372/338 392/389/355 479/474/441 f 392/389/355 342/345/311 491/484/451 f 492/501/468 493/485/452 495/487/454 f 493/485/452 496/488/455 494/486/453 f 480/475/442 477/471/438 497/489/456 f 477/471/438 476/470/437 494/486/453 f 496/488/455 498/490/457 497/489/456 f 500/494/461 501/492/459 498/490/457 f 501/492/459 484/493/460 499/491/458 f 480/475/442 497/489/456 482/476/443 f 501/492/459 500/494/461 503/496/463 f 504/498/465 505/497/464 502/495/462 f 505/497/464 488/481/448 503/496/463 f 484/493/460 501/492/459 486/479/446 f 505/497/464 504/498/465 507/500/467 f 506/499/466 492/501/468 507/500/467 f 476/470/437 490/483/450 495/487/454 f 488/481/448 505/497/464 490/483/450 f 508/517/484 509/502/469 511/504/471 f 509/502/469 512/505/472 510/503/470 f 31/24/23 277/284/250 513/506/473 f 167/166/143 511/504/471 277/284/250 f 512/505/472 514/507/474 513/506/473 f 514/507/474 516/509/476 515/508/475 f 517/510/477 18/39/38 515/508/475 f 31/24/23 513/506/473 32/25/24 f 516/509/476 518/511/478 517/510/477 f 518/511/478 520/513/480 519/512/479 f 53/1295/1263 51/40/39 521/514/481 f 18/39/38 517/510/477 51/40/39 f 520/513/480 522/515/482 521/514/481 f 522/515/482 508/517/484 523/516/483 f 167/166/143 166/165/142 511/504/471 f 53/1295/1263 521/514/481 166/165/142 f 527/522/489 524/518/485 526/520/487 f 528/523/490 529/521/488 525/519/486 f 529/521/488 512/505/472 526/520/487 f 508/517/484 527/522/489 509/502/469 f 529/521/488 528/523/490 531/525/492 f 530/524/491 532/526/493 531/525/492 f 516/509/476 514/507/474 533/527/494 f 512/505/472 529/521/488 514/507/474 f 532/526/493 534/528/495 533/527/494 f 534/528/495 536/530/497 535/529/496 f 520/513/480 518/511/478 537/531/498 f 518/511/478 516/509/476 535/529/496 f 536/530/497 538/532/499 537/531/498 f 524/518/485 527/522/489 538/532/499 f 527/522/489 508/517/484 539/533/500 f 522/515/482 520/513/480 539/533/500 f 543/542/509 540/534/501 542/536/503 f 541/535/502 544/537/504 542/536/503 f 545/538/505 546/539/506 542/536/503 f 547/540/507 548/541/508 542/536/503 f 63/555/522 62/48/47 550/544/511 f 62/48/47 61/47/46 549/543/510 f 296/328/294 294/303/269 551/545/512 f 292/302/268 550/544/511 294/303/269 f 551/545/512 61/47/46 552/546/513 f 58/45/44 57/44/43 552/546/513 f 307/1300/1265 325/329/295 553/547/514 f 296/328/294 551/545/512 325/329/295 f 553/547/514 57/44/43 554/548/515 f 65/50/49 555/549/516 60/51/50 f 288/297/263 308/313/279 555/549/516 f 307/1300/1265 553/547/514 308/313/279 f 557/552/519 548/541/508 556/550/517 f 547/540/507 546/539/506 556/550/517 f 292/302/268 289/298/264 558/551/518 f 288/297/263 557/552/519 289/298/264 f 558/551/518 546/539/506 559/553/520 f 544/537/504 560/554/521 545/538/505 f 560/554/521 63/555/522 559/553/520 f 292/302/268 558/551/518 550/544/511 f 564/564/531 561/556/523 563/558/525 f 562/557/524 565/559/526 563/558/525 f 566/560/527 567/561/528 563/558/525 f 568/562/529 569/563/530 563/558/525 f 540/534/501 543/542/509 571/566/533 f 543/542/509 548/541/508 570/565/532 f 288/297/263 555/549/516 557/552/519 f 555/549/516 65/50/49 570/565/532 f 572/605/572 573/567/534 575/569/536 f 576/592/559 577/570/537 573/567/534 f 528/523/490 525/519/486 577/570/537 f 525/519/486 524/518/485 574/568/535 f 576/592/559 578/571/538 577/570/537 f 580/591/558 581/573/540 578/571/538 f 581/573/540 532/526/493 579/572/539 f 530/524/491 528/523/490 579/572/539 f 580/591/558 582/574/541 581/573/540 f 584/606/573 585/576/543 582/574/541 f 585/576/543 536/530/497 583/575/542 f 532/526/493 581/573/540 534/528/495 f 584/606/573 586/577/544 585/576/543 f 572/605/572 575/569/536 586/577/544 f 524/518/485 538/532/499 575/569/536 f 536/530/497 585/576/543 538/532/499 f 591/587/554 588/579/546 590/581/548 f 589/580/547 592/582/549 590/581/548 f 593/583/550 594/584/551 590/581/548 f 595/585/552 596/586/553 590/581/548 f 210/615/582 209/204/181 598/589/556 f 208/203/180 599/590/557 209/204/181 f 599/590/557 580/591/558 597/588/555 f 578/571/538 576/592/559 597/588/555 f 603/601/568 600/593/560 602/595/562 f 601/594/561 604/596/563 602/595/562 f 605/597/564 606/598/565 602/595/562 f 607/599/566 608/600/567 602/595/562 f 204/200/177 207/207/184 610/603/570 f 212/206/183 611/604/571 207/207/184 f 611/604/571 572/605/572 609/602/569 f 586/577/544 584/606/573 609/602/569 f 612/623/590 613/607/574 615/609/576 f 616/611/578 617/610/577 613/607/574 f 617/610/577 576/592/559 614/608/575 f 572/605/572 615/609/576 573/567/534 f 617/610/577 616/611/578 619/613/580 f 620/616/583 621/614/581 618/612/579 f 621/614/581 210/615/582 619/613/580 f 576/592/559 617/610/577 598/589/556 f 621/614/581 620/616/583 623/618/585 f 622/617/584 624/619/586 623/618/585 f 212/206/183 211/205/182 625/620/587 f 211/205/182 210/615/582 623/618/585 f 624/619/586 626/621/588 625/620/587 f 626/621/588 612/623/590 627/622/589 f 572/605/572 611/604/571 615/609/576 f 611/604/571 212/206/183 627/622/589 f 428/632/599 427/422/388 629/625/592 f 426/421/387 630/626/593 427/422/388 f 584/606/573 582/574/541 630/626/593 f 582/574/541 580/591/558 628/624/591 f 630/626/593 426/421/387 631/627/594 f 433/429/395 632/628/595 434/430/396 f 632/628/595 204/200/177 631/627/594 f 584/606/573 630/626/593 610/603/570 f 433/429/395 440/435/401 632/628/595 f 440/435/401 439/439/405 633/629/596 f 634/630/597 208/203/180 633/629/596 f 204/200/177 632/628/595 205/201/178 f 439/439/405 443/438/404 634/630/597 f 443/438/404 428/632/599 635/631/598 f 580/591/558 599/590/557 629/625/592 f 599/590/557 208/203/180 635/631/598 f 636/653/620 637/633/600 639/635/602 f 637/633/600 640/636/603 638/634/601 f 616/611/578 613/607/574 641/637/604 f 613/607/574 612/623/590 638/634/601 f 641/637/604 640/636/603 643/639/606 f 644/641/608 645/640/607 642/638/605 f 645/640/607 620/616/583 643/639/606 f 616/611/578 641/637/604 618/612/579 f 645/640/607 644/641/608 647/643/610 f 648/645/612 649/644/611 646/642/609 f 649/644/611 624/619/586 647/643/610 f 620/616/583 645/640/607 622/617/584 f 649/644/611 648/645/612 651/647/614 f 636/653/620 639/635/602 650/646/613 f 612/623/590 626/621/588 639/635/602 f 626/621/588 624/619/586 651/647/614 f 652/728/695 653/648/615 655/650/617 f 653/648/615 656/651/618 654/649/616 f 640/636/603 637/633/600 657/652/619 f 637/633/600 636/653/620 654/649/616 f 656/651/618 658/654/621 657/652/619 f 660/657/624 661/656/623 658/654/621 f 661/656/623 644/641/608 659/655/622 f 642/638/605 640/636/603 659/655/622 f 661/656/623 660/657/624 663/659/626 f 664/661/628 665/660/627 662/658/625 f 665/660/627 648/645/612 663/659/626 f 644/641/608 661/656/623 646/642/609 f 665/660/627 664/661/628 667/663/630 f 652/728/695 655/650/617 666/662/629 f 655/650/617 636/653/620 667/663/630 f 648/645/612 665/660/627 650/646/613 f 671/668/635 668/664/631 670/666/633 f 672/669/636 673/667/634 669/665/632 f 65/50/49 64/49/48 673/667/634 f 64/49/48 63/555/522 670/666/633 f 673/667/634 672/669/636 675/671/638 f 676/673/640 677/672/639 674/670/637 f 677/672/639 540/534/501 675/671/638 f 65/50/49 673/667/634 571/566/533 f 677/672/639 676/673/640 679/675/642 f 678/674/641 680/676/643 679/675/642 f 544/537/504 541/535/502 681/677/644 f 540/534/501 677/672/639 541/535/502 f 680/676/643 682/678/645 681/677/644 f 682/678/645 668/664/631 683/679/646 f 63/555/522 560/554/521 671/668/635 f 560/554/521 544/537/504 683/679/646 f 684/694/661 685/680/647 687/682/649 f 685/680/647 688/683/650 686/681/648 f 689/684/651 672/669/636 686/681/648 f 668/664/631 687/682/649 669/665/632 f 689/684/651 688/683/650 691/686/653 f 692/688/655 693/687/654 690/685/652 f 693/687/654 676/673/640 691/686/653 f 672/669/636 689/684/651 674/670/637 f 693/687/654 692/688/655 695/690/657 f 696/711/678 697/691/658 694/689/656 f 680/676/643 678/674/641 697/691/658 f 676/673/640 693/687/654 678/674/641 f 696/711/678 698/692/659 697/691/658 f 698/692/659 684/694/661 699/693/660 f 668/664/631 682/678/645 687/682/649 f 682/678/645 680/676/643 699/693/660 f 703/699/666 700/695/662 702/697/664 f 704/700/667 705/698/665 701/696/663 f 688/683/650 685/680/647 705/698/665 f 685/680/647 684/694/661 702/697/664 f 705/698/665 704/700/667 707/702/669 f 706/701/668 708/703/670 707/702/669 f 709/704/671 692/688/655 707/702/669 f 688/683/650 705/698/665 690/685/652 f 708/703/670 710/705/672 709/704/671 f 710/705/672 712/707/674 711/706/673 f 696/711/678 694/689/656 713/708/675 f 692/688/655 709/704/671 694/689/656 f 712/707/674 714/709/676 713/708/675 f 700/695/662 703/699/666 714/709/676 f 684/694/661 698/692/659 703/699/666 f 698/692/659 696/711/678 715/710/677 f 719/716/683 716/712/679 718/714/681 f 720/717/684 721/715/682 717/713/680 f 721/715/682 656/651/618 718/714/681 f 652/728/695 719/716/683 653/648/615 f 721/715/682 720/717/684 723/719/686 f 722/718/685 724/720/687 723/719/686 f 725/721/688 660/657/624 723/719/686 f 656/651/618 721/715/682 658/654/621 f 724/720/687 726/722/689 725/721/688 f 726/722/689 728/724/691 727/723/690 f 664/661/628 662/658/625 729/725/692 f 662/658/625 660/657/624 727/723/690 f 729/725/692 728/724/691 731/727/694 f 716/712/679 719/716/683 730/726/693 f 719/716/683 652/728/695 731/727/694 f 664/661/628 729/725/692 666/662/629 f 732/746/713 733/729/696 735/731/698 f 733/729/696 736/732/699 734/730/697 f 26/18/18 410/405/371 737/733/700 f 410/405/371 151/150/128 734/730/697 f 736/732/699 738/734/701 737/733/700 f 740/738/705 741/736/703 738/734/701 f 741/736/703 7/737/704 739/735/702 f 26/18/18 737/733/700 27/19/19 f 741/736/703 740/738/705 743/740/707 f 744/854/821 745/741/708 742/739/706 f 745/741/708 149/742/709 743/740/707 f 7/737/704 741/736/703 746/743/710 f 744/854/821 747/744/711 745/741/708 f 747/744/711 732/746/713 748/745/712 f 151/150/128 150/149/127 735/731/698 f 149/742/709 745/741/708 150/149/127 f 749/985/953 750/747/714 752/749/716 f 750/747/714 753/750/717 751/748/715 f 20/13/13 56/43/42 754/751/718 f 56/43/42 55/144/122 751/748/715 f 753/750/717 755/752/719 754/751/718 f 755/752/719 757/754/721 756/753/720 f 758/755/722 22/416/382 756/753/720 f 20/13/13 754/751/718 21/14/14 f 758/755/722 757/754/721 760/757/724 f 761/760/727 762/758/725 759/756/723 f 762/758/725 142/759/726 760/757/724 f 22/416/382 758/755/722 418/413/379 f 762/758/725 761/760/727 764/762/729 f 749/985/953 752/749/716 763/761/728 f 55/144/122 143/143/121 752/749/716 f 142/759/726 762/758/725 143/143/121 f 765/778/745 766/763/730 768/765/732 f 766/763/730 769/766/733 767/764/731 f 770/767/734 704/700/667 767/764/731 f 700/695/662 768/765/732 701/696/663 f 769/766/733 771/768/735 770/767/734 f 773/771/738 774/770/737 771/768/735 f 708/703/670 706/701/668 774/770/737 f 704/700/667 770/767/734 706/701/668 f 774/770/737 773/771/738 776/773/740 f 777/775/742 778/774/741 775/772/739 f 712/707/674 710/705/672 778/774/741 f 710/705/672 708/703/670 776/773/740 f 778/774/741 777/775/742 780/777/744 f 779/776/743 765/778/745 780/777/744 f 768/765/732 700/695/662 780/777/744 f 714/709/676 712/707/674 780/777/744 f 781/794/761 782/779/746 784/781/748 f 782/779/746 785/782/749 783/780/747 f 769/766/733 766/763/730 786/783/750 f 766/763/730 765/778/745 783/780/747 f 785/782/749 787/784/751 786/783/750 f 789/787/754 790/786/753 787/784/751 f 790/786/753 773/771/738 788/785/752 f 771/768/735 769/766/733 788/785/752 f 790/786/753 789/787/754 792/789/756 f 793/791/758 794/790/757 791/788/755 f 794/790/757 777/775/742 792/789/756 f 773/771/738 790/786/753 775/772/739 f 794/790/757 793/791/758 796/793/760 f 795/792/759 781/794/761 796/793/760 f 765/778/745 779/776/743 784/781/748 f 777/775/742 794/790/757 779/776/743 f 797/810/777 798/795/762 800/797/764 f 798/795/762 801/798/765 799/796/763 f 785/782/749 782/779/746 802/799/766 f 782/779/746 781/794/761 799/796/763 f 801/798/765 803/800/767 802/799/766 f 805/803/770 806/802/769 803/800/767 f 806/802/769 789/787/754 804/801/768 f 787/784/751 785/782/749 804/801/768 f 806/802/769 805/803/770 808/805/772 f 809/807/774 810/806/773 807/804/771 f 810/806/773 793/791/758 808/805/772 f 789/787/754 806/802/769 791/788/755 f 810/806/773 809/807/774 812/809/776 f 811/808/775 797/810/777 812/809/776 f 781/794/761 795/792/759 800/797/764 f 793/791/758 810/806/773 795/792/759 f 814/813/780 569/563/530 813/811/778 f 568/562/529 567/561/528 813/811/778 f 801/798/765 798/795/762 815/812/779 f 798/795/762 797/810/777 813/811/778 f 815/812/779 567/561/528 816/814/781 f 566/560/527 565/559/526 816/814/781 f 817/815/782 805/803/770 816/814/781 f 803/800/767 801/798/765 816/814/781 f 817/815/782 565/559/526 818/816/783 f 562/557/524 561/556/523 818/816/783 f 819/817/784 809/807/774 818/816/783 f 805/803/770 817/815/782 807/804/771 f 819/817/784 561/556/523 820/818/785 f 564/564/531 569/563/530 820/818/785 f 797/810/777 811/808/775 814/813/780 f 809/807/774 819/817/784 811/808/775 f 822/821/788 596/586/553 821/819/786 f 595/585/552 594/584/551 821/819/786 f 720/717/684 717/713/680 823/820/787 f 717/713/680 716/712/679 821/819/786 f 823/820/787 594/584/551 824/822/789 f 593/583/550 592/582/549 824/822/789 f 724/720/687 722/718/685 825/823/790 f 720/717/684 823/820/787 722/718/685 f 825/823/790 592/582/549 826/824/791 f 589/580/547 588/579/546 826/824/791 f 827/825/792 728/724/691 826/824/791 f 724/720/687 825/823/790 726/722/689 f 827/825/792 588/579/546 828/826/793 f 591/587/554 596/586/553 828/826/793 f 822/821/788 716/712/679 828/826/793 f 728/724/691 827/825/792 730/726/693 f 832/831/798 829/827/794 831/829/796 f 833/1158/1127 834/830/797 830/828/795 f 736/732/699 733/729/696 834/830/797 f 733/729/696 732/746/713 831/829/796 f 833/1158/1127 835/832/799 834/830/797 f 835/832/799 837/834/801 836/833/800 f 740/738/705 738/734/701 838/835/802 f 738/734/701 736/732/699 836/833/800 f 842/844/811 839/836/803 841/838/805 f 840/837/804 843/839/806 841/838/805 f 844/840/807 845/841/808 841/838/805 f 846/842/809 847/843/810 841/838/805 f 851/848/815 848/845/812 850/847/814 f 829/827/794 832/831/798 849/846/813 f 732/746/713 747/744/711 832/831/798 f 744/854/821 851/848/815 747/744/711 f 853/851/818 847/843/810 852/849/816 f 846/842/809 845/841/808 852/849/816 f 744/854/821 742/739/706 854/850/817 f 740/738/705 853/851/818 742/739/706 f 845/841/808 844/840/807 854/850/817 f 844/840/807 843/839/806 855/852/819 f 848/845/812 851/848/815 856/853/820 f 851/848/815 744/854/821 855/852/819 f 856/853/820 843/839/806 857/855/822 f 840/837/804 839/836/803 857/855/822 f 837/834/801 859/857/824 858/856/823 f 859/857/824 848/845/812 857/855/822 f 858/856/823 839/836/803 860/858/825 f 847/843/810 853/851/818 842/844/811 f 853/851/818 740/738/705 860/858/825 f 837/834/801 858/856/823 838/835/802 f 864/864/832 861/859/826 863/861/828 f 862/860/827 865/862/829 863/861/828 f 159/157/135 227/224/831 866/863/830 f 9/6/6 864/864/832 227/224/831 f 865/862/829 867/865/833 866/863/830 f 869/891/859 870/867/835 867/865/833 f 149/742/709 193/189/166 870/867/835 f 159/157/135 866/863/830 193/189/166 f 869/891/859 871/868/836 870/867/835 f 871/868/836 873/870/838 872/869/837 f 7/737/704 746/743/710 874/871/839 f 149/742/709 870/867/835 746/743/710 f 873/870/838 875/872/840 874/871/839 f 861/859/826 864/864/832 875/872/840 f 9/6/6 8/5/5 864/864/832 f 8/5/5 7/737/704 876/873/841 f 878/876/844 41/32/31 877/874/842 f 40/31/30 39/30/29 877/874/842 f 865/862/829 862/860/827 879/875/843 f 861/859/826 878/876/844 862/860/827 f 879/875/843 39/30/29 880/877/845 f 37/899/867 881/878/846 38/29/28 f 869/891/859 867/865/833 881/878/846 f 867/865/833 865/862/829 880/877/845 f 882/1291/1260 883/879/847 885/881/849 f 886/1288/1257 887/882/850 883/879/847 f 888/1284/1253 889/883/851 887/882/850 f 890/1282/1251 885/881/849 889/883/851 f 33/26/25 36/33/32 892/885/853 f 36/33/32 41/32/31 891/884/852 f 878/876/844 861/859/826 891/884/852 f 873/870/838 892/885/853 875/872/840 f 893/902/870 894/886/854 896/888/856 f 894/886/854 897/889/857 895/887/855 f 873/870/838 871/868/836 898/890/858 f 871/868/836 869/891/859 895/887/855 f 897/889/857 899/892/860 898/890/858 f 901/895/863 902/894/862 899/892/860 f 902/894/862 33/26/25 900/893/861 f 873/870/838 898/890/858 892/885/853 f 902/894/862 901/895/863 904/897/865 f 905/1303/1268 906/898/866 903/896/864 f 906/898/866 37/899/867 904/897/865 f 33/26/25 902/894/862 34/27/26 f 905/1303/1268 907/900/868 906/898/866 f 907/900/868 893/902/870 908/901/869 f 869/891/859 881/878/846 896/888/856 f 881/878/846 37/899/867 908/901/869 f 909/923/891 910/903/871 912/905/873 f 910/903/871 913/906/874 911/904/872 f 897/889/857 894/886/854 914/907/875 f 893/902/870 912/905/873 894/886/854 f 913/906/874 915/908/876 914/907/875 f 915/908/876 917/910/878 916/909/877 f 901/895/863 899/892/860 918/911/879 f 899/892/860 897/889/857 916/909/877 f 918/911/879 917/910/878 920/913/881 f 921/915/883 922/914/882 919/912/880 f 905/1303/1268 903/896/864 922/914/882 f 901/895/863 918/911/879 903/896/864 f 922/914/882 921/915/883 924/917/885 f 909/923/891 912/905/873 923/916/884 f 912/905/873 893/902/870 924/917/885 f 905/1303/1268 922/914/882 907/900/868 f 925/934/902 926/918/886 928/920/888 f 926/918/886 929/921/889 927/919/887 f 913/906/874 910/903/871 930/922/890 f 910/903/871 909/923/891 927/919/887 f 929/921/889 931/924/892 930/922/890 f 933/927/895 934/926/894 931/924/892 f 934/926/894 917/910/878 932/925/893 f 915/908/876 913/906/874 932/925/893 f 934/926/894 933/927/895 936/929/897 f 937/931/899 938/930/898 935/928/896 f 938/930/898 921/915/883 936/929/897 f 917/910/878 934/926/894 919/912/880 f 938/930/898 937/931/899 940/933/901 f 939/932/900 925/934/902 940/933/901 f 909/923/891 923/916/884 928/920/888 f 921/915/883 938/930/898 923/916/884 f 941/950/918 942/935/903 944/937/905 f 942/935/903 945/938/906 943/936/904 f 259/266/232 256/263/229 946/939/907 f 256/263/229 255/279/245 943/936/904 f 945/938/906 947/940/908 946/939/907 f 949/943/911 950/942/910 947/940/908 f 950/942/910 263/271/237 948/941/909 f 261/268/234 259/266/232 948/941/909 f 950/942/910 949/943/911 952/945/913 f 953/947/915 954/946/914 951/944/912 f 954/946/914 267/276/242 952/945/913 f 263/271/237 950/942/910 265/272/238 f 954/946/914 953/947/915 956/949/917 f 955/948/916 941/950/918 956/949/917 f 255/279/245 269/277/243 944/937/905 f 267/276/242 954/946/914 269/277/243 f 958/954/922 178/951/919 957/952/920 f 177/175/152 176/174/151 957/952/920 f 959/953/921 945/938/906 957/952/920 f 941/950/918 958/954/922 942/935/903 f 959/953/921 176/174/151 960/955/923 f 175/173/150 174/172/149 960/955/923 f 949/943/911 947/940/908 961/956/924 f 945/938/906 959/953/921 947/940/908 f 961/956/924 174/172/149 962/957/925 f 171/169/146 170/958/926 962/957/925 f 953/947/915 951/944/912 963/959/927 f 951/944/912 949/943/911 962/957/925 f 963/959/927 170/958/926 964/960/928 f 173/171/148 178/951/919 964/960/928 f 958/954/922 941/950/918 964/960/928 f 955/948/916 953/947/915 964/960/928 f 966/963/931 608/600/567 965/961/929 f 607/599/566 606/598/565 965/961/929 f 967/962/930 448/445/411 965/961/929 f 444/440/406 966/963/931 445/441/407 f 967/962/930 606/598/565 968/964/932 f 605/597/564 604/596/563 968/964/932 f 452/448/414 450/446/412 969/965/933 f 448/445/411 967/962/930 450/446/412 f 604/596/563 601/594/561 969/965/933 f 601/594/561 600/593/560 970/966/934 f 456/453/419 454/450/416 971/967/935 f 454/450/416 452/448/414 970/966/934 f 971/967/935 600/593/560 972/968/936 f 603/601/568 608/600/567 972/968/936 f 966/963/931 444/440/406 972/968/936 f 458/454/420 456/453/419 972/968/936 f 976/973/941 973/969/937 975/971/939 f 977/974/942 978/972/940 974/970/938 f 978/972/940 753/750/717 975/971/939 f 749/985/953 976/973/941 750/747/714 f 978/972/940 977/974/942 980/976/944 f 979/975/943 981/977/945 980/976/944 f 757/754/721 755/752/719 982/978/946 f 755/752/719 753/750/717 980/976/944 f 981/977/945 983/979/947 982/978/946 f 983/979/947 985/981/949 984/980/948 f 761/760/727 759/756/723 986/982/950 f 759/756/723 757/754/721 984/980/948 f 985/981/949 987/983/951 986/982/950 f 973/969/937 976/973/941 987/983/951 f 976/973/941 749/985/953 988/984/952 f 763/761/728 761/760/727 988/984/952 f 989/1234/1203 990/986/954 992/988/956 f 990/986/954 993/989/957 991/987/955 f 994/990/958 995/991/959 991/987/955 f 996/992/960 997/993/961 991/987/955 f 1001/999/967 998/994/962 1000/996/964 f 999/995/963 1002/997/965 1000/996/964 f 981/977/945 979/975/943 1003/998/966 f 977/974/942 1001/999/967 979/975/943 f 1003/998/966 1002/997/965 1005/1001/969 f 1004/1000/968 1006/1002/970 1005/1001/969 f 985/981/949 983/979/947 1007/1003/971 f 983/979/947 981/977/945 1005/1001/969 f 1007/1003/971 1006/1002/970 1009/1005/973 f 1008/1004/972 1010/1006/974 1009/1005/973 f 1011/1007/975 973/969/937 1009/1005/973 f 987/983/951 985/981/949 1009/1005/973 f 348/348/314 347/1008/976 1012/1009/977 f 378/376/342 336/1010/978 1012/1009/977 f 334/339/305 332/336/302 1012/1009/977 f 329/334/300 328/333/299 1012/1009/977 f 183/1037/1005 1013/1011/979 184/179/156 f 1013/1011/979 1015/1013/981 1014/1012/980 f 408/403/369 405/400/366 1016/1014/982 f 405/400/366 185/180/157 1014/1012/980 f 1015/1013/981 1017/1015/983 1016/1014/982 f 1017/1015/983 1019/1017/985 1018/1016/984 f 413/409/375 411/406/372 1020/1018/986 f 411/406/372 408/403/369 1018/1016/984 f 1020/1018/986 1019/1017/985 1022/1020/988 f 179/176/153 182/182/159 1021/1019/987 f 182/182/159 187/414/380 1022/1020/988 f 413/409/375 1020/1018/986 415/410/376 f 1026/1025/993 1023/1021/989 1025/1023/991 f 1027/1026/994 1028/1024/992 1024/1022/990 f 1028/1024/992 1015/1013/981 1025/1023/991 f 183/1037/1005 1026/1025/993 1013/1011/979 f 1028/1024/992 1027/1026/994 1030/1028/996 f 1029/1027/995 1031/1029/997 1030/1028/996 f 1019/1017/985 1017/1015/983 1032/1030/998 f 1015/1013/981 1028/1024/992 1017/1015/983 f 1031/1029/997 1033/1031/999 1032/1030/998 f 1033/1031/999 1035/1033/1001 1034/1032/1000 f 179/176/153 1021/1019/987 1036/1034/1002 f 1021/1019/987 1019/1017/985 1034/1032/1000 f 1035/1033/1001 1037/1035/1003 1036/1034/1002 f 1023/1021/989 1026/1025/993 1037/1035/1003 f 1026/1025/993 183/1037/1005 1038/1036/1004 f 180/177/154 179/176/153 1038/1036/1004 f 1039/1053/1021 1040/1038/1006 1042/1040/1008 f 1040/1038/1006 1043/1041/1009 1041/1039/1007 f 1044/1042/1010 1027/1026/994 1041/1039/1007 f 1023/1021/989 1042/1040/1008 1024/1022/990 f 1043/1041/1009 1045/1043/1011 1044/1042/1010 f 1047/1046/1014 1048/1045/1013 1045/1043/1011 f 1031/1029/997 1029/1027/995 1048/1045/1013 f 1027/1026/994 1044/1042/1010 1029/1027/995 f 1048/1045/1013 1047/1046/1014 1050/1048/1016 f 1051/1050/1018 1052/1049/1017 1049/1047/1015 f 1035/1033/1001 1033/1031/999 1052/1049/1017 f 1033/1031/999 1031/1029/997 1050/1048/1016 f 1052/1049/1017 1051/1050/1018 1054/1052/1020 f 1053/1051/1019 1039/1053/1021 1054/1052/1020 f 1042/1040/1008 1023/1021/989 1054/1052/1020 f 1037/1035/1003 1035/1033/1001 1054/1052/1020 f 1056/1058/1026 50/1054/1022 1055/1055/1023 f 49/38/37 48/1056/1024 1055/1055/1023 f 1043/1041/1009 1040/1038/1006 1057/1057/1025 f 1040/1038/1006 1039/1053/1021 1055/1055/1023 f 1057/1057/1025 48/1056/1024 1058/1059/1027 f 47/37/36 46/1060/1028 1058/1059/1027 f 1059/1061/1029 1047/1046/1014 1058/1059/1027 f 1045/1043/1011 1043/1041/1009 1058/1059/1027 f 1059/1061/1029 46/1060/1028 1060/1062/1030 f 43/34/33 42/1063/1031 1060/1062/1030 f 1061/1064/1032 1051/1050/1018 1060/1062/1030 f 1047/1046/1014 1059/1061/1029 1049/1047/1015 f 1061/1064/1032 42/1063/1031 1062/1065/1033 f 45/36/35 50/1054/1022 1062/1065/1033 f 1039/1053/1021 1053/1051/1019 1056/1058/1026 f 1051/1050/1018 1061/1064/1032 1053/1051/1019 f 1064/1070/1038 401/1066/1034 1063/1067/1035 f 400/394/360 399/1068/1036 1063/1067/1035 f 496/488/455 493/485/452 1065/1069/1037 f 493/485/452 492/501/468 1063/1067/1035 f 1065/1069/1037 399/1068/1036 1066/1071/1039 f 398/393/359 397/1072/1040 1066/1071/1039 f 1067/1073/1041 500/494/461 1066/1071/1039 f 498/490/457 496/488/455 1066/1071/1039 f 1067/1073/1041 397/1072/1040 1068/1074/1042 f 394/390/356 393/1075/1043 1068/1074/1042 f 1069/1076/1044 504/498/465 1068/1074/1042 f 500/494/461 1067/1073/1041 502/495/462 f 1069/1076/1044 393/1075/1043 1070/1077/1045 f 396/392/358 401/1066/1034 1070/1077/1045 f 492/501/468 506/499/466 1064/1070/1038 f 504/498/465 1069/1076/1044 506/499/466 f 1074/1082/1050 1071/1078/1046 1073/1080/1048 f 1075/1083/1051 1076/1081/1049 1072/1079/1047 f 1076/1081/1049 346/469/436 1073/1080/1048 f 344/461/427 1074/1082/1050 345/347/313 f 1076/1081/1049 1075/1083/1051 1078/1085/1053 f 1077/1084/1052 1079/1086/1054 1078/1085/1053 f 1080/1087/1055 381/1088/1056 1078/1085/1053 f 346/469/436 1076/1081/1049 475/468/435 f 1079/1086/1054 1081/1089/1057 1080/1087/1055 f 1081/1089/1057 1083/1091/1059 1082/1090/1058 f 385/382/348 382/379/345 1084/1092/1060 f 382/379/345 381/1088/1056 1082/1090/1058 f 1084/1092/1060 1083/1091/1059 1086/1094/1062 f 1071/1078/1046 1074/1082/1050 1085/1093/1061 f 344/461/427 464/460/426 1074/1082/1050 f 464/460/426 385/382/348 1086/1094/1062 f 1090/1099/1067 1087/1095/1063 1089/1097/1065 f 1091/1118/1086 1092/1098/1066 1088/1096/1064 f 1092/1098/1066 1075/1083/1051 1089/1097/1065 f 1071/1078/1046 1090/1099/1067 1072/1079/1047 f 1091/1118/1086 1093/1100/1068 1092/1098/1066 f 1093/1100/1068 1095/1102/1070 1094/1101/1069 f 1079/1086/1054 1077/1084/1052 1096/1103/1071 f 1075/1083/1051 1092/1098/1066 1077/1084/1052 f 1095/1102/1070 1097/1104/1072 1096/1103/1071 f 1097/1104/1072 1099/1106/1074 1098/1105/1073 f 1083/1091/1059 1081/1089/1057 1100/1107/1075 f 1081/1089/1057 1079/1086/1054 1098/1105/1073 f 1100/1107/1075 1099/1106/1074 1102/1109/1077 f 1087/1095/1063 1090/1099/1067 1101/1108/1076 f 1090/1099/1067 1071/1078/1046 1102/1109/1077 f 1083/1091/1059 1100/1107/1075 1085/1093/1061 f 1103/1126/1094 1104/1110/1078 1106/1112/1080 f 1104/1110/1078 1107/1113/1081 1105/1111/1079 f 1091/1118/1086 1088/1096/1064 1108/1114/1082 f 1088/1096/1064 1087/1095/1063 1105/1111/1079 f 1107/1113/1081 1109/1115/1083 1108/1114/1082 f 1111/1119/1087 1112/1117/1085 1109/1115/1083 f 1112/1117/1085 1095/1102/1070 1110/1116/1084 f 1093/1100/1068 1091/1118/1086 1110/1116/1084 f 1112/1117/1085 1111/1119/1087 1114/1121/1089 f 1115/1123/1091 1116/1122/1090 1113/1120/1088 f 1116/1122/1090 1099/1106/1074 1114/1121/1089 f 1095/1102/1070 1112/1117/1085 1097/1104/1072 f 1116/1122/1090 1115/1123/1091 1118/1125/1093 f 1117/1124/1092 1103/1126/1094 1118/1125/1093 f 1087/1095/1063 1101/1108/1076 1106/1112/1080 f 1099/1106/1074 1116/1122/1090 1101/1108/1076 f 1122/1131/1099 1119/1127/1095 1121/1129/1097 f 1123/1132/1100 1124/1130/1098 1120/1128/1096 f 1124/1130/1098 1107/1113/1081 1121/1129/1097 f 1104/1110/1078 1103/1126/1094 1121/1129/1097 f 1124/1130/1098 1123/1132/1100 1126/1134/1102 f 1127/1149/1118 1128/1135/1103 1125/1133/1101 f 1128/1135/1103 1111/1119/1087 1126/1134/1102 f 1107/1113/1081 1124/1130/1098 1109/1115/1083 f 1127/1149/1118 1129/1136/1104 1128/1135/1103 f 1129/1136/1104 1131/464/1106 1130/1137/1105 f 1115/1123/1091 1113/1120/1088 1132/1138/1107 f 1113/1120/1088 1111/1119/1087 1130/1137/1105 f 1131/464/1106 1133/1139/1108 1132/1138/1107 f 1133/1139/1108 1119/1127/1095 1134/1140/1109 f 1103/1126/1094 1117/1124/1092 1122/1131/1099 f 1117/1124/1092 1115/1123/1091 1134/1140/1109 f 1136/1145/1114 473/1141/1110 1135/1142/1111 f 472/466/433 471/1143/1112 1135/1142/1111 f 1123/1132/1100 1120/1128/1096 1137/1144/1113 f 1120/1128/1096 1119/1127/1095 1135/1142/1111 f 1137/1144/1113 471/1143/1112 1138/1146/1115 f 470/465/431 469/1147/1116 1138/1146/1115 f 1139/1148/1117 1127/1149/1118 1138/1146/1115 f 1123/1132/1100 1137/1144/1113 1125/1133/1101 f 1139/1148/1117 469/1147/1116 1140/1150/1119 f 466/462/428 465/1151/1120 1140/1150/1119 f 1141/1152/1121 1131/464/1106 1140/1150/1119 f 1127/1149/1118 1139/1148/1117 1129/1136/1104 f 1141/1152/1121 465/1151/1120 1142/1153/1122 f 468/464/430 473/1141/1110 1142/1153/1122 f 1119/1127/1095 1133/1139/1108 1136/1145/1114 f 1133/1139/1108 1131/464/1106 1142/1153/1122 f 1146/1159/1128 1143/1154/1123 1145/1156/1125 f 1147/1160/1129 1148/1157/1126 1144/1155/1124 f 1148/1157/1126 833/1158/1127 1145/1156/1125 f 829/827/794 1146/1159/1128 830/828/795 f 1148/1157/1126 1147/1160/1129 1150/1162/1131 f 1149/1161/1130 1151/1163/1132 1150/1162/1131 f 837/834/801 835/832/799 1152/1164/1133 f 835/832/799 833/1158/1127 1150/1162/1131 f 1151/1163/1132 1153/1165/1134 1152/1164/1133 f 1153/1165/1134 1155/1167/1136 1154/1166/1135 f 848/845/812 859/857/824 1156/1168/1137 f 837/834/801 1152/1164/1133 859/857/824 f 1155/1167/1136 1157/1169/1138 1156/1168/1137 f 1143/1154/1123 1146/1159/1128 1157/1169/1138 f 1146/1159/1128 829/827/794 1158/1170/1139 f 848/845/812 1156/1168/1137 849/846/813 f 304/1185/1154 1159/1171/1140 1161/1173/1142 f 1159/1171/1140 1162/1174/1143 1160/1172/1141 f 1147/1160/1129 1144/1155/1124 1163/1175/1144 f 1144/1155/1124 1143/1154/1123 1160/1172/1141 f 1162/1174/1143 1164/1176/1145 1163/1175/1144 f 1166/1179/1148 1167/1178/1147 1164/1176/1145 f 1167/1178/1147 1151/1163/1132 1165/1177/1146 f 1149/1161/1130 1147/1160/1129 1165/1177/1146 f 1167/1178/1147 1166/1179/1148 1169/1181/1150 f 306/1183/1152 1170/1182/1151 1168/1180/1149 f 1170/1182/1151 1155/1167/1136 1169/1181/1150 f 1151/1163/1132 1167/1178/1147 1153/1165/1134 f 1170/1182/1151 306/1183/1152 1171/1184/1153 f 305/311/277 304/1185/1154 1171/1184/1153 f 1143/1154/1123 1157/1169/1138 1161/1173/1142 f 1155/1167/1136 1170/1182/1151 1157/1169/1138 f 302/309/275 355/353/319 303/310/276 f 355/353/319 354/359/325 1172/1186/1155 f 1162/1174/1143 1159/1171/1140 1173/1187/1156 f 1159/1171/1140 304/1185/1154 1172/1186/1155 f 354/359/325 359/358/324 1173/1187/1156 f 358/357/323 1175/1189/1158 359/358/324 f 1175/1189/1158 1166/1179/1148 1174/1188/1157 f 1164/1176/1145 1162/1174/1143 1174/1188/1157 f 1175/1189/1158 358/357/323 1176/1190/1159 f 298/306/272 301/312/278 362/362/328 f 301/312/278 306/1183/1152 1176/1190/1159 f 1166/1179/1148 1175/1189/1158 1168/1180/1149 f 1008/1004/972 1006/1002/970 1177/1191/1160 f 1004/1000/968 1002/997/965 1177/1191/1160 f 999/995/963 998/994/962 1177/1191/1160 f 1010/1006/974 1008/1004/972 1178/1192/1161 f 1182/1197/1166 1179/1193/1162 1181/1195/1164 f 1183/1210/1179 1184/1196/1165 1180/1194/1163 f 1184/1196/1165 977/974/942 1181/1195/1164 f 973/969/937 1182/1197/1166 974/970/938 f 1183/1210/1179 1185/1198/1167 1184/1196/1165 f 1187/1201/1170 1188/1200/1169 1185/1198/1167 f 998/994/962 1001/999/967 1188/1200/1169 f 977/974/942 1184/1196/1165 1001/999/967 f 1188/1200/1169 1187/1201/1170 1190/1203/1172 f 1191/1205/1174 1192/1204/1173 1189/1202/1171 f 1192/1204/1173 1010/1006/974 1190/1203/1172 f 998/994/962 1188/1200/1169 1178/1192/1161 f 1192/1204/1173 1191/1205/1174 1194/1207/1176 f 1179/1193/1162 1182/1197/1166 1193/1206/1175 f 973/969/937 1011/1007/975 1182/1197/1166 f 1010/1006/974 1192/1204/1173 1011/1007/975 f 1196/1211/1180 997/993/961 1195/1208/1177 f 996/992/960 995/991/959 1195/1208/1177 f 1197/1209/1178 1183/1210/1179 1195/1208/1177 f 1179/1193/1162 1196/1211/1180 1180/1194/1163 f 1197/1209/1178 995/991/959 1198/1212/1181 f 994/990/958 993/989/957 1198/1212/1181 f 1187/1201/1170 1185/1198/1167 1199/1213/1182 f 1185/1198/1167 1183/1210/1179 1198/1212/1181 f 1203/1222/1191 1200/1214/1183 1202/1216/1185 f 1201/1215/1184 1204/1217/1186 1202/1216/1185 f 1205/1218/1187 1206/1219/1188 1202/1216/1185 f 1207/1220/1189 1208/1221/1190 1202/1216/1185 f 989/1234/1203 992/988/956 1210/1224/1193 f 992/988/956 997/993/961 1209/1223/1192 f 1179/1193/1162 1193/1206/1175 1196/1211/1180 f 1191/1205/1174 1210/1224/1193 1193/1206/1175 f 1214/1229/1198 1211/1225/1194 1213/1227/1196 f 1215/1230/1199 1216/1228/1197 1212/1226/1195 f 1216/1228/1197 1191/1205/1174 1213/1227/1196 f 1187/1201/1170 1214/1229/1198 1189/1202/1171 f 1216/1228/1197 1215/1230/1199 1218/1232/1201 f 1219/1255/1224 1220/1233/1202 1217/1231/1200 f 1220/1233/1202 989/1234/1203 1218/1232/1201 f 1191/1205/1174 1216/1228/1197 1210/1224/1193 f 1219/1255/1224 1221/1235/1204 1220/1233/1202 f 1221/1235/1204 1223/1237/1206 1222/1236/1205 f 993/989/957 990/986/954 1224/1238/1207 f 990/986/954 989/1234/1203 1222/1236/1205 f 1223/1237/1206 1225/1239/1208 1224/1238/1207 f 1225/1239/1208 1211/1225/1194 1226/1240/1209 f 1187/1201/1170 1199/1213/1182 1214/1229/1198 f 1199/1213/1182 993/989/957 1226/1240/1209 f 1230/1245/1214 1227/1241/1210 1229/1243/1212 f 1231/1246/1215 1232/1244/1213 1228/1242/1211 f 1232/1244/1213 1215/1230/1199 1229/1243/1212 f 1211/1225/1194 1230/1245/1214 1212/1226/1195 f 1232/1244/1213 1231/1246/1215 1234/1248/1217 f 1233/1247/1216 1235/1249/1218 1234/1248/1217 f 1219/1255/1224 1217/1231/1200 1236/1250/1219 f 1215/1230/1199 1232/1244/1213 1217/1231/1200 f 1235/1249/1218 1237/1251/1220 1236/1250/1219 f 1237/1251/1220 1239/1253/1222 1238/1252/1221 f 1223/1237/1206 1221/1235/1204 1240/1254/1223 f 1221/1235/1204 1219/1255/1224 1238/1252/1221 f 1239/1253/1222 1241/1256/1225 1240/1254/1223 f 1241/1256/1225 1227/1241/1210 1242/1257/1226 f 1211/1225/1194 1225/1239/1208 1230/1245/1214 f 1225/1239/1208 1223/1237/1206 1242/1257/1226 f 1246/1262/1231 1243/1258/1227 1245/1260/1229 f 1247/1263/1232 1248/1261/1230 1244/1259/1228 f 1248/1261/1230 1231/1246/1215 1245/1260/1229 f 1227/1241/1210 1246/1262/1231 1228/1242/1211 f 1248/1261/1230 1247/1263/1232 1250/1265/1234 f 1251/1278/1247 1252/1266/1235 1249/1264/1233 f 1252/1266/1235 1235/1249/1218 1250/1265/1234 f 1231/1246/1215 1248/1261/1230 1233/1247/1216 f 1251/1278/1247 1253/1267/1236 1252/1266/1235 f 1253/1267/1236 1255/1269/1238 1254/1268/1237 f 1239/1253/1222 1237/1251/1220 1256/1270/1239 f 1237/1251/1220 1235/1249/1218 1254/1268/1237 f 1255/1269/1238 1257/1271/1240 1256/1270/1239 f 1257/1271/1240 1243/1258/1227 1258/1272/1241 f 1227/1241/1210 1241/1256/1225 1246/1262/1231 f 1241/1256/1225 1239/1253/1222 1258/1272/1241 f 1260/1275/1244 1208/1221/1190 1259/1273/1242 f 1207/1220/1189 1206/1219/1188 1259/1273/1242 f 1261/1274/1243 1247/1263/1232 1259/1273/1242 f 1243/1258/1227 1260/1275/1244 1244/1259/1228 f 1261/1274/1243 1206/1219/1188 1262/1276/1245 f 1205/1218/1187 1204/1217/1186 1262/1276/1245 f 1263/1277/1246 1251/1278/1247 1262/1276/1245 f 1247/1263/1232 1261/1274/1243 1249/1264/1233 f 1263/1277/1246 1204/1217/1186 1264/1279/1248 f 1201/1215/1184 1200/1214/1183 1264/1279/1248 f 1255/1269/1238 1253/1267/1236 1265/1280/1249 f 1253/1267/1236 1251/1278/1247 1264/1279/1248 f 1265/1280/1249 1200/1214/1183 1266/1281/1250 f 1203/1222/1191 1208/1221/1190 1266/1281/1250 f 1243/1258/1227 1257/1271/1240 1260/1275/1244 f 1257/1271/1240 1255/1269/1238 1266/1281/1250 f 1268/1286/1255 890/1282/1251 1267/1283/1252 f 889/883/851 888/1284/1253 1267/1283/1252 f 929/921/889 926/918/886 1269/1285/1254 f 926/918/886 925/934/902 1267/1283/1252 f 1269/1285/1254 888/1284/1253 1270/1287/1256 f 887/882/850 886/1288/1257 1270/1287/1256 f 933/927/895 931/924/892 1271/1289/1258 f 931/924/892 929/921/889 1270/1287/1256 f 1271/1289/1258 886/1288/1257 1272/1290/1259 f 883/879/847 882/1291/1260 1272/1290/1259 f 1273/1292/1261 937/931/899 1272/1290/1259 f 933/927/895 1271/1289/1258 935/928/896 f 1273/1292/1261 882/1291/1260 1274/1293/1262 f 885/881/849 890/1282/1251 1274/1293/1262 f 925/934/902 939/932/900 1268/1286/1255 f 937/931/899 1273/1292/1261 939/932/900 ================================================ FILE: static/models/target.obj ================================================ # Blender v2.76 (sub 0) OBJ File: 'target.blend' # www.blender.org o Cylinder_Cylinder.002 v 0.000000 0.000000 -0.319792 v 0.062388 0.000000 -0.313648 v -0.062388 0.000000 -0.313648 v -0.122379 0.000000 -0.295450 v -0.177667 0.000000 -0.265898 v -0.226127 0.000000 -0.226128 v -0.265897 0.000000 -0.177667 v -0.295449 0.000000 -0.122380 v -0.313648 0.000000 -0.062389 v -0.319792 0.000000 -0.000000 v -0.313648 0.000000 0.062388 v -0.295450 0.000000 0.122379 v -0.265898 0.000000 0.177667 v -0.226127 0.000000 0.226127 v -0.177667 0.000000 0.265898 v -0.122379 0.000000 0.295450 v -0.062389 0.000000 0.313648 v -0.000000 0.000000 0.319792 v -0.000000 0.000000 0.291944 v -0.056955 0.000000 0.286334 v -0.111722 0.000000 0.269721 v -0.162195 0.000000 0.242742 v -0.206435 0.000000 0.206435 v -0.242742 0.000000 0.162195 v -0.269721 0.000000 0.111722 v -0.286334 0.000000 0.056955 v -0.291943 0.000000 -0.000000 v -0.286334 0.000000 -0.056956 v -0.269720 0.000000 -0.111722 v -0.242742 0.000000 -0.162195 v -0.206435 0.000000 -0.206435 v -0.162195 0.000000 -0.242742 v -0.111722 0.000000 -0.269721 v -0.056955 0.000000 -0.286334 v -0.000000 0.000000 -0.291943 v 0.056955 0.000000 -0.286334 v 0.111722 0.000000 -0.269721 v 0.162195 0.000000 -0.242742 v 0.206435 0.000000 -0.206435 v 0.242742 0.000000 -0.162195 v 0.269721 0.000000 -0.111722 v 0.286334 0.000000 -0.056955 v 0.291943 0.000000 0.000000 v 0.286334 0.000000 0.056955 v 0.269721 0.000000 0.111722 v 0.242742 0.000000 0.162195 v 0.206435 0.000000 0.206435 v 0.162195 0.000000 0.242742 v 0.111722 0.000000 0.269721 v 0.056955 0.000000 0.286334 v 0.062388 0.000000 0.313648 v 0.122379 0.000000 0.295450 v 0.177667 0.000000 0.265898 v 0.226127 0.000000 0.226127 v 0.265898 0.000000 0.177667 v 0.295450 0.000000 0.122379 v 0.313648 0.000000 0.062388 v 0.319792 0.000000 0.000000 v 0.313648 0.000000 -0.062388 v 0.295450 0.000000 -0.122379 v 0.265898 0.000000 -0.177667 v 0.226127 0.000000 -0.226127 v 0.177667 0.000000 -0.265898 v 0.122379 0.000000 -0.295450 v -0.035452 0.000000 0.007052 v -0.033395 0.000000 0.013833 v -0.030055 0.000000 0.020082 v -0.025560 0.000000 0.025560 v -0.020082 0.000000 0.030055 v -0.013833 0.000000 0.033395 v -0.007052 0.000000 0.035452 v -0.000000 0.000000 0.036147 v 0.007052 0.000000 0.035452 v 0.013833 0.000000 0.033395 v 0.020082 0.000000 0.030055 v 0.025560 0.000000 0.025560 v 0.030055 0.000000 0.020082 v 0.033395 0.000000 0.013833 v 0.035452 0.000000 0.007052 v 0.036147 0.000000 0.000000 v 0.035452 0.000000 -0.007052 v 0.033395 0.000000 -0.013833 v 0.030055 0.000000 -0.020082 v 0.025560 0.000000 -0.025559 v 0.020082 0.000000 -0.030055 v 0.013833 0.000000 -0.033395 v 0.007052 0.000000 -0.035452 v -0.000000 0.000000 -0.036147 v -0.007052 0.000000 -0.035452 v -0.013833 0.000000 -0.033395 v -0.020082 0.000000 -0.030055 v -0.025559 0.000000 -0.025559 v -0.030055 0.000000 -0.020082 v -0.033395 0.000000 -0.013833 v -0.035452 0.000000 -0.007052 v -0.036147 0.000000 0.000000 s off f 10 27 28 f 44 58 43 f 80 88 96 f 2 1 35 f 3 4 33 f 5 6 31 f 7 8 29 f 9 10 28 f 11 12 26 f 13 14 24 f 15 16 22 f 17 18 19 f 35 36 2 f 17 19 20 f 34 35 1 f 17 20 21 f 34 1 3 f 17 21 16 f 33 34 3 f 16 21 22 f 32 33 4 f 15 22 23 f 32 4 5 f 15 23 14 f 31 32 5 f 14 23 24 f 30 31 6 f 13 24 25 f 30 6 7 f 13 25 12 f 29 30 7 f 12 25 26 f 28 29 8 f 11 26 27 f 28 8 9 f 11 27 10 f 64 2 37 f 50 19 18 f 2 36 37 f 49 50 51 f 64 37 38 f 50 18 51 f 64 38 63 f 48 49 52 f 63 38 39 f 49 51 52 f 63 39 62 f 47 48 53 f 62 39 40 f 48 52 53 f 62 40 61 f 46 47 54 f 61 40 41 f 47 53 54 f 61 41 60 f 45 46 55 f 60 41 42 f 46 54 55 f 60 42 59 f 44 45 56 f 59 42 43 f 45 55 56 f 59 43 58 f 57 58 44 f 44 56 57 f 96 65 68 f 66 67 68 f 68 69 70 f 70 71 72 f 72 73 76 f 74 75 76 f 76 77 78 f 78 79 76 f 80 81 84 f 82 83 84 f 84 85 86 f 86 87 84 f 88 89 92 f 90 91 92 f 92 93 94 f 94 95 92 f 65 66 68 f 68 70 96 f 73 74 76 f 76 79 80 f 81 82 84 f 84 87 88 f 89 90 92 f 92 95 96 f 96 70 72 f 72 76 96 f 80 84 88 f 88 92 96 f 96 76 80 ================================================ FILE: style/splash.scss ================================================ @font-face { font-family: 'PlatformMedium'; src: url('/static/fonts/Platform-Medium-Web.eot'); src: url('/static/fonts/Platform-Medium-Web.eot?#iefix') format('embedded-opentype'), url('/static/fonts/Platform-Medium-Web.woff2') format('woff2'), url('/static/fonts/Platform-Medium-Web.woff') format('woff'); } @font-face { font-family: 'PlatformRegular'; src: url('/static/fonts/Platform-Regular-Web.eot'); src: url('/static/fonts/Platform-Regular-Web.eot?#iefix') format('embedded-opentype'), url('/static/fonts/Platform-Regular-Web.woff2') format('woff2'), url('/static/fonts/Platform-Regular-Web.woff') format('woff'); } @font-face { font-family: 'PlatformLight'; src: url('/static/fonts/Platform-Light-Web.eot'); src: url('/static/fonts/Platform-Light-Web.eot?#iefix') format('embedded-opentype'), url('/static/fonts/Platform-Light-Web.woff2') format('woff2'), url('/static/fonts/Platform-Light-Web.woff') format('woff'); } html, body{ height: 100%; font-family: 'PlatformRegular', sans-serif; letter-spacing: 0.05em; color: white; overflow:hidden; -webkit-tap-highlight-color: transparent; } @mixin mobilePhone { @media (max-width:540px), (max-height:540px){ @content; } } // used to hide navbars in older mobile browsers #spacer { height: 1px; } #splash { z-index:1000; width: 100%; height: 100%; position: absolute; top: 0px; left: 0px; display: block; overflow:hidden; &.invisible { display: none; } // ---------------------------------------------------- // background image // ---------------------------------------------------- @media screen and (max-aspect-ratio: 1/1){ background-image: url(/static/img/SplashMobileHiRes2.jpg); } $hiResCross : 1080px; @media screen and (min-aspect-ratio: 1/1) and (max-width: $hiResCross){ background-image: url(/static/img/SplashDesktopMedR.jpg); } @media screen and (min-aspect-ratio: 1/1) and (min-width: $hiResCross){ background-image: url(/static/img/SplashDesktopHiR.jpg) ; } @media screen and (max-height: 600px){ background-position-y: -50px; } background-position-x: center; background-position-y: center; background-repeat: no-repeat; background-attachment: fixed; background-size: cover; background-color: rgb(255,161,153); // ---------------------------------------------------- // enter buttons // ---------------------------------------------------- #webvr-loader { } $buttonWidth : 195px; $buttonHeight : 55px; #enter-container { position: absolute; left: 50%; bottom: 25%; @media screen and (max-height: 320px){ bottom: 60px !important; transform: scale(0.6) translate(-80%, 0); } height: $buttonHeight; width: $buttonWidth; transform: translate(-50%, 0); pointer-events: none; &.loaded { pointer-events: initial; #enterButton { opacity: 1; } #loader { opacity: 0; } } #loader { position: absolute; width: 100%; height: 100%; line-height: $buttonHeight; border: 2px solid white; box-sizing: border-box; padding-left: 45px; font-weight: 500; letter-spacing: 0.05em; img { position: absolute; right: 40px; top: 48%; transform: translate(0, -50%); width: $buttonHeight * 0.5; height: $buttonHeight * 0.45; } } #loader, #enterButton{ transition: opacity 0.2s; } #enterButton { font-family: 'PlatformRegular', sans-serif; letter-spacing: 0.05em; display: block; font-size: 16px; width: 100%; height: 100%; opacity: 0; .webvr-ui-button { border: 2px solid white; position: absolute; transform: translate(-50%, 0)!important; top: 0px!important; background-color: transparent; white-space: nowrap; .webvr-ui-title { font-size: 16px; } } #enter-360{ position: absolute; left: 50%; bottom: -30px; transform: translate(-50%, 0); width: 100%; font-size: 14px; text-align: center; width: 220px; text-decoration: underline; cursor: pointer; @include mobilePhone { font-size: 12px; } } } } // ---------------------------------------------------- // about secondary // ---------------------------------------------------- $headphoneSize : 25px; $topCornerMargin : 20px; $buttonSize : 30px; $pink : rgb(255,175,170); $mediumSize : 700px; $desktopMargin : 20%; $mobileMargin : 5%; #headphones { position: absolute; left: $topCornerMargin; top: $topCornerMargin + 5px; width: $headphoneSize; height: $headphoneSize; overflow: hidden; transition: width 0.15s; &:hover { width: 210px; } .icon { height: 90%; width: auto; position: absolute; left: 3.5px; } .text { line-height: $buttonSize; height: $buttonSize; font-size: 12px; width: 170px; position: absolute; text-align: right; left: $headphoneSize + 5px; } } #headphones, #about-button { background-color: white; border-radius: $buttonSize/2; width: $buttonSize; height: $buttonSize; color: $pink; text-align: center; line-height: $buttonSize; opacity: 0.9; } #about-button { z-index: 1; top: $topCornerMargin; right: $topCornerMargin; visibility: hidden; padding: 1px; position: absolute; cursor: pointer; font-family: 'PlatformLight', sans-serif; font-size: 20px; img { width: 30px; } } #about-button.visible { visibility: visible; } // ---------------------------------------------------- // about // ---------------------------------------------------- #about { z-index: 1; overflow: hidden; width: 100%; height: 100%; overflow: auto; position: absolute !important; visibility: hidden; background-color: rgba(247,158,151,0.97); z-index: 1000; display: flex; align-items: center; justify-content: center; font-size: calc(0.8em) !important; .content { margin-top: -0vh; margin-right:$desktopMargin; margin-left: $desktopMargin; @media (max-width:$mediumSize) { margin-right:$mobileMargin; margin-left: $mobileMargin; } } h1 { font-family: 'PlatformMedium', sans-serif; font-weight: 100; font-size: 7.0vw; line-height: 0%; } .description { left:0px; margin-left:10px; @media (max-width:$mediumSize) { margin-left:0px; } margin-top: -0vh; text-decoration: none; p { font-family: 'PlatformLight', sans-serif; max-width: 50%; font-weight: 100; line-height: 140%; color: #FFF; @media (max-width:$mediumSize) { max-width: 100%; } a { font-weight: 300; text-decoration: underline; color: #8D81E8; } } } img { z-index:-1; position: absolute; top: 45%; right: 0px; margin-right:25%; width: 20%; height: auto; display:block; @media (max-width:$mediumSize) { top: initial; left: 50%; transform: translateX(-50%); bottom: 0%; width: 50%; } @media screen and (orientation:portrait) and (max-width:$mediumSize) { display: block; } @media screen and (orientation:landscape) and (max-width:$mediumSize){ display: none; } } .xbutton { width:20px; height:20px; font-size: 20px; border: none; position: fixed; top: 20px; right: 20px; cursor: pointer; } .close { // position: fixed; // width: 32px; // height: 32px; // opacity: 0.75; } .close:hover { opacity: 1; } .close:before, .close:after { position: absolute; left: 15px; content: ' '; height: 33px; width: 2px; background-color: #FFF; } .close:before { transform: rotate(45deg); } .close:after { transform: rotate(-45deg); } } #about.visible { visibility: visible; } // ---------------------------------------------------- // badges // ---------------------------------------------------- #badges { .friends-with, .webvr-logo, .pipe { position: absolute; bottom: 17px; } .friends-with img, .webvr-logo img { width: 100%; } .webvr-logo { width: 100px; left: 15px; } .friends-with { width: 100px; left: 145px; } .pipe { left: 130px; height:60px; border-right: 1px solid rgba(255,255,255,0.75); } @include mobilePhone { .friends-with img, .webvr-logo img { width: 70%; } .friends-with { left: 115px; width: 100px; } .pipe { left: 100px; height:45px; border-right: 1px solid rgba(255,255,255,0.75); } } } #legal { opacity: 0.75; position:absolute; right: 20px; bottom: 21px; font-family: 'PlatformLight', sans-serif; font-size: 11px; color: #FFF; a { text-decoration: none; color: #FFF; } @include mobilePhone { font-size: 9px; } } } @mixin fullScreenBg() { z-index:1000; width: 100%; height: 100%; position: absolute; top: 0px; left: 0px; overflow:hidden; background-position-x: center; background-position-y: center; background-repeat: no-repeat; background-attachment: fixed; background-size: cover; background-color: rgba(247,158,151,1.0); } #cardboard { @include fullScreenBg(); @media screen and (orientation:portrait) { display: block; } @media screen and (orientation:landscape) { display: none; } #cardboardContainer { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: "#f79e97"; p { font-weight: 100; text-decoration: none; line-height: 150%; color: #FFF; } button { cursor: pointer; font-family: 'PlatformMedium', sans-serif; letter-spacing: 0.05em; font-size: 13px; color: white; padding-left:20px; padding-right:20px; height: 50px; background-color: rgba(253,161,155,0.75); border: white 2px solid; position: absolute; bottom: 0px; right: 0px; } img{ display: block; min-width:340px; min-height:244px; width: 100%; height: 100%; } } } #error { @include fullScreenBg(); display: block; #errorContainer { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); min-width:320px; min-height:240px; background-color: "#ff0"; p { position: absolute; padding: 0px; margin: 0px; top: 0px; left: 0px; max-width:200px; } img{ max-width:400px; max-height:300px; width: 100%; height: 100%; position: absolute; top: 72%; left: 50%; transform: translate(-50%, -50%); } button { cursor: pointer; font-family: 'PlatformMedium', sans-serif; letter-spacing: 0.05em; font-size: 13px; color: white; position: absolute; padding-left:20px; padding-right:20px; height: 50px; background-color: rgba(253,161,155,0.75); border: white 2px solid; top: 0px; right: 0px; } } } ================================================ FILE: third_party/aframe-daydream-controller-component/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Kevin Ngo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: third_party/aframe-daydream-controller-component/METADATA ================================================ name: "aframe-daydream-controller-component" description: "Aframe component for daydream" third_party { url { type: GIT value: "https://github.com/ryanbetts/aframe-daydream-controller-component" } version: "" last_upgrade_date { year: 2017 month: 3 day: 13 } } ================================================ FILE: third_party/aframe-daydream-controller-component/daydream-controller.js ================================================ import OrientationArmModel from '../../js/orientation-arm-model' if (typeof AFRAME === 'undefined') { throw new Error('Component attempted to register before AFRAME was available.'); } var bind = AFRAME.utils.bind; var trackedControlsUtils = AFRAME.utils.trackedControls; var GAMEPAD_ID_PREFIX = 'Daydream Controller'; /** * Daydream Controller component for A-Frame. */ AFRAME.registerComponent('daydream-controller', { armModel: null, /** * Set if component needs multiple instancing. */ multiple: false, schema: { controller: { default: 0 }, id: { default: 'Match none by default!' }, rotationOffset: { default: 0 }, hand: { default: 'left' }, buttonColor: { default: '#FAFAFA' }, // Off-white. buttonTouchedColor: { default: 'yellow' }, // Light blue. buttonPressedColor: { default: 'orange' }, // Light blue. model: { default: true }, rotationOffset: { default: 0 }, // use -999 as sentinel value to auto-determine based on hand gestureTimeoutLimit: { default: 100 }, //if gesture doesn't complete within this timeframe, reset gestureTolerance: { default: 0.2 } //percentage of the trackpad a gesture must traverse }, // buttonId // 0 - trackpad mapping: { axis0: 'trackpad', axis1: 'trackpad', button0: 'trackpad', // button1: 'menu', // button2: 'system' }, bindMethods: function() { this.onModelLoaded = bind(this.onModelLoaded, this); this.onControllersUpdate = bind(this.onControllersUpdate, this); this.checkIfControllerPresent = bind(this.checkIfControllerPresent, this); this.removeControllersUpdateListener = bind(this.removeControllersUpdateListener, this); this.onGamepadConnected = bind(this.onGamepadConnected, this); this.onGamepadDisconnected = bind(this.onGamepadDisconnected, this); }, /** * Called once when component is attached. Generally for initial setup. */ init: function() { var self = this; this.animationActive = 'pointing'; this.onButtonChanged = bind(this.onButtonChanged, this); this.onButtonDown = function(evt) { self.onButtonEvent(evt.detail.id, 'down'); }; this.onButtonUp = function(evt) { self.onButtonEvent(evt.detail.id, 'up'); }; this.onButtonTouchStart = function(evt) { self.onButtonEvent(evt.detail.id, 'touchstart'); evt.stopPropagation(); evt.preventDefault(); }; this.onButtonTouchEnd = function(evt) { self.onButtonEvent(evt.detail.id, 'touchend'); }; this.onAxisMove = bind(this.onAxisMove, this); this.controllerPresent = false; this.everGotGamepadEvent = false; this.lastControllerCheck = 0; this.bindMethods(); this.isControllerPresent = trackedControlsUtils.isControllerPresent; // to allow mock this.axisGestureTimeoutLimit = 100; //minimum this.axisGestureVelocity = 100; // minimum velocity in %/ms this.axisGestureThreshold = 0.1; // minimum % moved to recognize a gesture this.buttonStates = {}; this.previousAxis = []; this.previousControllerPosition = new THREE.Vector3(); this.armModel = new OrientationArmModel(); // var camera = document.querySelector("#avatar"); // camera = camera.object3D; // this.armModel.setHeadPosition(camera.position); this.armModel.setHeadPosition({x:0,y:1.6,z:0}); }, addEventListeners: function() { var el = this.el; el.addEventListener('buttonchanged', this.onButtonChanged); el.addEventListener('buttondown', this.onButtonDown); el.addEventListener('buttonup', this.onButtonUp); el.addEventListener('touchstart', this.onButtonTouchStart); el.addEventListener('axismove', this.onAxisMove); el.addEventListener('touchend', this.onButtonTouchEnd); el.addEventListener('model-loaded', this.onModelLoaded); }, removeEventListeners: function() { var el = this.el; el.removeEventListener('buttonchanged', this.onButtonChanged); el.removeEventListener('buttondown', this.onButtonDown); el.removeEventListener('buttonup', this.onButtonUp); el.removeEventListener('touchstart', this.onButtonTouchStart); el.removeEventListener('axismove', this.onAxisMove); el.removeEventListener('touchend', this.onButtonTouchEnd); el.removeEventListener('model-loaded', this.onModelLoaded); }, /** * Called when a component is removed (e.g., via removeAttribute). * Generally undoes all modifications to the entity. */ // TODO ... remove: function () { }, getControllerIfPresent: function() { // The 'Gear VR Touchpad' gamepad exposed by Carmel has no pose, // so it won't show up in the tracked-controls system controllers. var gamepads = this.getGamepadsByPrefix(GAMEPAD_ID_PREFIX); if (!gamepads || !gamepads.length) { return undefined; } return gamepads[0]; }, checkIfControllerPresent: function() { var data = this.data; var isPresent = this.isControllerPresent(this.el.sceneEl, GAMEPAD_ID_PREFIX, {}); if (isPresent === this.controllerPresent) { return; } this.controllerPresent = isPresent; if (isPresent) { this.injectTrackedControls(); // inject track-controls this.addEventListeners(); } else { this.removeEventListeners(); } }, onGamepadConnected: function(evt) { // for now, don't disable controller update listening, due to // apparent issue with FF Nightly only sending one event and seeing one controller; // this.everGotGamepadEvent = true; // this.removeControllersUpdateListener(); this.checkIfControllerPresent(); }, onGamepadDisconnected: function(evt) { // for now, don't disable controller update listening, due to // apparent issue with FF Nightly only sending one event and seeing one controller; // this.everGotGamepadEvent = true; // this.removeControllersUpdateListener(); this.checkIfControllerPresent(); }, tick: function() { var mesh = this.el.getObject3D('mesh'); // Update mesh animations. if (mesh && mesh.update) { mesh.update(delta / 1000); } this.updatePose(); this.updateButtons(); }, /** * Called when entity resumes. * Use to continue or add any dynamic or background behavior such as events. */ play: function() { this.checkIfControllerPresent(); window.addEventListener('gamepadconnected', this.onGamepadConnected, false); window.addEventListener('gamepaddisconnected', this.onGamepadDisconnected, false); this.addControllersUpdateListener(); }, /** * Called when entity pauses. * Use to stop or remove any dynamic or background behavior such as events. */ pause: function() { window.removeEventListener('gamepadconnected', this.onGamepadConnected, false); window.removeEventListener('gamepaddisconnected', this.onGamepadDisconnected, false); this.removeControllersUpdateListener(); this.removeEventListeners(); }, injectTrackedControls: function() { var el = this.el; var data = this.data; this.controller = trackedControlsUtils.getGamepadsByPrefix(GAMEPAD_ID_PREFIX)[0] // if we have an OpenVR Gamepad, use the fixed mapping // el.setAttribute('tracked-controls', {id: GAMEPAD_ID_PREFIX, rotationOffset: data.rotationOffset}); if (!data.model) { return; } }, addControllersUpdateListener: function() { this.el.sceneEl.addEventListener('controllersupdated', this.onControllersUpdate, false); }, removeControllersUpdateListener: function() { this.el.sceneEl.removeEventListener('controllersupdated', this.onControllersUpdate, false); }, onControllersUpdate: function() { if (!this.everGotGamepadEvent) { this.checkIfControllerPresent(); } }, onButtonChanged: function(evt) { var button = this.mapping['button' + evt.detail.id]; var buttonMeshes = this.buttonMeshes; var value; value = evt.detail.state.value; }, onAxisMove: function(evt) { // this.axisPosition // // console.log('axismove', evt.detail); // this.lastAxisMovement = { // time: Date.now(), // x: 0, // y: 0 // } }, onModelLoaded: function(evt) { var controllerObject3D = evt.detail.model; var buttonMeshes; if (!this.data.model) { return; } buttonMeshes = this.buttonMeshes = {}; buttonMeshes.menu = controllerObject3D.getObjectByName('menubutton'); buttonMeshes.system = controllerObject3D.getObjectByName('systembutton'); buttonMeshes.trackpad = controllerObject3D.getObjectByName('touchpad'); // Offset pivot point controllerObject3D.position.set(0, -0.015, 0.04); }, onButtonEvent: function(id, evtName) { var buttonName = this.mapping['button' + id]; if(!buttonName) { return; } var i; if (Array.isArray(buttonName)) { for (i = 0; i < buttonName.length; i++) { this.el.emit(buttonName[i] + evtName); } } else { this.el.emit(buttonName + evtName); } // this.updateModel(buttonName, evtName); }, updateModel: function(buttonName, evtName) { var i; if (!this.data.model) { return; } if (Array.isArray(buttonName)) { for (i = 0; i < buttonName.length; i++) { this.updateButtonModel(buttonName[i], evtName); } } else { this.updateButtonModel(buttonName, evtName); } }, updateButtonModel: function(buttonName, state) { var color = this.data.buttonColor; if (state === 'touchstart' || state === 'up') { color = this.data.buttonTouchedColor; } else if (state === 'down') { color = this.data.buttonPressedColor; } var buttonMeshes = this.buttonMeshes; if (!buttonMeshes) { return; } buttonMeshes[buttonName].material.color.set(color); }, /* */ updatePose: (function() { var controllerEuler = new THREE.Euler(); var controllerPosition = new THREE.Vector3(); var controllerQuaternion = new THREE.Quaternion(); var deltaControllerPosition = new THREE.Vector3(); var dolly = new THREE.Object3D(); var standingMatrix = new THREE.Matrix4(); controllerEuler.order = 'YXZ'; return function() { var camera = document.querySelector("#avatar"); if(!camera) return; camera = camera.object3D; var pose; var orientation; var position; var el = this.el; var controller = this.controller; if (!this.controller) { return; } pose = controller.pose; orientation = pose.orientation || [0, 0, 0, 1]; position = pose.position || [0, 0, 0]; // var camera = this.el.sceneEl.camera; controllerQuaternion.fromArray(orientation); // Feed camera and controller into the arm model. this.armModel.setHeadOrientation(camera.quaternion); // no need to set the head position anymore because it is located inside camera this.armModel.setHeadPosition({x:0,y:1.6,z:0}); this.armModel.setControllerOrientation(controllerQuaternion); this.armModel.update(); // Get resulting pose and configure the renderer. var modelPose = this.armModel.getPose(); controllerEuler.setFromQuaternion(modelPose.orientation) el.setAttribute('rotation', { x: THREE.Math.radToDeg(controllerEuler.x), y: THREE.Math.radToDeg(controllerEuler.y), z: THREE.Math.radToDeg(controllerEuler.z) + this.data.rotationOffset }); // console.log(modelPose.position); el.setAttribute('position', { x: modelPose.position.x, y: modelPose.position.y, z: modelPose.position.z }); } })(), updateButtons: function() { var i; var buttonState; var controller = this.controller; if (!this.controller) { return; } for (i = 0; i < controller.buttons.length; ++i) { buttonState = controller.buttons[i]; this.handleButton(i, buttonState); } this.handleAxes(controller.axes); }, handleAxes: function(controllerAxes) { var previousAxis = this.previousAxis; var changed = false; var i; for (i = 0; i < controllerAxes.length; ++i) { if (previousAxis[i] !== controllerAxes[i]) { changed = true; break; } } if (!changed) { return; } this.previousAxis = controllerAxes.slice(); this.el.emit('axismove', { axis: this.previousAxis }); }, handleButton: function(id, buttonState) { var changed = false; changed = changed || this.handlePress(id, buttonState); changed = changed || this.handleTouch(id, buttonState); changed = changed || this.handleValue(id, buttonState); if (!changed) { return; } this.el.emit('buttonchanged', { id: id, state: buttonState }); }, /** * Determine whether a button press has occured and emit events as appropriate. * * @param {string} id - id of the button to check. * @param {object} buttonState - state of the button to check. * @returns {boolean} true if button press state changed, false otherwise. */ handlePress: function(id, buttonState) { var buttonStates = this.buttonStates; var evtName; var previousButtonState = buttonStates[id] = buttonStates[id] || {}; if (buttonState.pressed === previousButtonState.pressed) { return false; } if (buttonState.pressed) { evtName = 'down'; } else { evtName = 'up'; } this.el.emit('button' + evtName, { id: id }); previousButtonState.pressed = buttonState.pressed; return true; }, /** * Determine whether a button touch has occured and emit events as appropriate. * * @param {string} id - id of the button to check. * @param {object} buttonState - state of the button to check. * @returns {boolean} true if button touch state changed, false otherwise. */ handleTouch: function(id, buttonState) { var buttonStates = this.buttonStates; var evtName; var previousButtonState = buttonStates[id] = buttonStates[id] || {}; if (buttonState.touched === previousButtonState.touched) { return false; } if (buttonState.touched) { evtName = 'start'; } else { evtName = 'end'; } previousButtonState.touched = buttonState.touched; var touches = []; this.el.emit('touch' + evtName, { id: id, state: previousButtonState, touches: touches }); return true; }, /** * Determine whether a button value has changed. * * @param {string} id - id of the button to check. * @param {object} buttonState - state of the button to check. * @returns {boolean} true if button value changed, false otherwise. */ handleValue: function(id, buttonState) { var buttonStates = this.buttonStates; var previousButtonState = buttonStates[id] = buttonStates[id] || {}; if (buttonState.value === previousButtonState.value) { return false; } previousButtonState.value = buttonState.value; return true; } });