Skip to content

Instantly share code, notes, and snippets.

@BorisKourt
Last active June 2, 2020 08:37
Show Gist options
  • Save BorisKourt/e7e5ebf24fa1ecde701535d2df255edd to your computer and use it in GitHub Desktop.
Save BorisKourt/e7e5ebf24fa1ecde701535d2df255edd to your computer and use it in GitHub Desktop.
This script can load a session and insert or update the transforms of the entities in it.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using UnityEngine;
/**
* KuratorState is a global Singleton that handles local state management.
*
* NOTE: Not all state needs to be saved to disk. This class should also
* be used for ephemeral current state needs.
*
* NOTE: This system is currently deeply reliant on a network connection.
* In the future it should be able to rely more on the local cache.
*/
/**
* This is the data that will be saved to disk.
*/
[Serializable]
class KuratorData {
public string device_id;
}
public class KuratorState : MonoBehaviour
{
// Singleton. Globally available via KuratorState.v
public static KuratorState v;
// This is the identifier for this device.
// It is loaded once per install.
// NOTE: getters and setters can have side-effects.
private string _device_id;
public string device_id {
get {
if (_device_id == null) {
// TODO: See if this should be a runtime error.
return null;
} else {
return _device_id;
}
}
set {
if (_device_id == null) {
_device_id = value;
// Once the device_id is available provide a list of sessions for that device.
// FetchData.get_sessions_for_device.
} else {
Debug.Log("Preventing Overwrite of device_id. Please use overwrite_device_id method if this is absolutely necessary.");
}
}
}
// Current Collection.
// TODO: Allow the user to select the collection in the future:
private string _collection_id = "d5ad46ad-a326-4c3d-b94b-3351b2d677eb";
public string collection_id {
get {
return _collection_id;
}
set {
_collection_id = value;
}
}
// Current Session ID.
private string _session_id;
public string session_id {
get {
return _session_id;
}
set {
_session_id = value;
}
}
// A list of all the sessions started by this device.
private List<string> _session_ids;
public List<string> session_ids {
get {
return _session_ids;
}
set {
_session_ids = value;
}
}
/* Function: Awake
* ------------------
* NOTE: Instantiate any Singletons on Awake.
* Then make sure everything that needs them runs
* on or afeter Start().
*/
void Awake() {
if (v == null) {
// Don't destroy the first instance on Scene transitions.
DontDestroyOnLoad(gameObject);
// Set the static reference to this instance.
v = this;
} else if (v != this) {
// Destroy any duplicates in other scenes.
Destroy(gameObject);
}
}
/* Function: Start
* ------------------
* As load_or_create needs to access the Mutations Singleton
* we run this on Start.
*/
void Start() {
// Run cross-Singleton code after everything 'wakes up'.
load_or_create();
}
void OnDisable() {
// Make sure that this isn't a duplicate.
if (v == this) {
save();
}
}
private void save() {
BinaryFormatter bf = new BinaryFormatter();
FileStream file = File.Create(Application.persistentDataPath + "/kuratorstate.bin");
KuratorData data = new KuratorData();
// Explicitly update data to save:
data.device_id = device_id;
bf.Serialize(file, data);
file.Close();
}
private void load() {
BinaryFormatter bf = new BinaryFormatter();
FileStream file = File.Open(Application.persistentDataPath + "/kuratorstate.bin", FileMode.Open);
KuratorData data = (KuratorData)bf.Deserialize(file);
file.Close();
// Explicitly set local data fields:
device_id = data.device_id;
}
/* Function: create
* ------------------
* Setup any needed state.
*
* NOTE: Currently if going through the Mutation workflow,
* these calls are Async. Perhaps this class should
* provide the callbacks rather than Mutations refering to
* this class.
* TODO: Evaluate if this is desired behavior.
*/
private void create() {
Mutations.m.insert_device_mutation();
}
private void load_or_create() {
if(File.Exists(Application.persistentDataPath + "/kuratorstate.bin")) {
load();
} else {
create();
}
}
/* Function: overwrite_device_id
* ------------------
* As the device id really shouldn't change, this is separated out in case
* that becomes necessary.
*/
public void overwrite_device_id(string id) {
_device_id = id;
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using GraphQlClient.Core;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
public class LoadSession : MonoBehaviour
{
public GraphApi Graph;
public string session_id = "825188ae-1f1b-41d2-8c4d-d3e5e8a7e6f4";
private string base_url = "https://kurator-core.herokuapp.com";
void Start() { }
/* Function: create_blank_entity
* ------------------
* Returns a new entity with all the required base data, components and sub-objects.
*/
private GameObject create_blank_entity(string entity_id) {
GameObject entity = GameObject.CreatePrimitive(PrimitiveType.Plane);
entity.name = entity_id;
// Provide any scripts via:
// entity.AddComponent<LocationEntity>();
return entity;
}
/* Function: get_entity_unit_ratio
* ------------------
* Hardcoded Kurator Core UNIT enum conventions.
* If the the GraphQL schema is extended, this also needs to be
* updated.
*/
private float get_entity_unit_ratio(string unit) {
switch(unit) {
case "MM":
return 1000.0f;
case "CM":
return 100.0f;
case "M":
return 1.0f;
case "KM":
return 0.01f;
default:
Debug.Log("ISSUE: Unknown unit: " + unit + "! Defaulting to CM");
return 100.0f;
}
}
/* Function: dimensions_to_unity_size
* ------------------
* Process Kurator Core conventions for Units into usable Unity sizes.
*/
private (float width, float height, float depth) dimensions_to_unity_size(JToken dimensions) {
float ratio = 0;
float width = 0;
float depth = 0;
float height = 0;
if (dimensions["width"].Type != JTokenType.Null && dimensions["width_unit"].Type != JTokenType.Null) {
ratio = get_entity_unit_ratio(dimensions["width_unit"].ToString());
width = dimensions["width"].ToObject<float>() / ratio;
}
if (dimensions["height"].Type != JTokenType.Null && dimensions["height_unit"].Type != JTokenType.Null ) {
ratio = get_entity_unit_ratio(dimensions["height_unit"].ToString());
height = dimensions["height"].ToObject<float>() / ratio;
}
if (dimensions["depth"].Type != JTokenType.Null && dimensions["depth_unit"].Type != JTokenType.Null ) {
ratio = get_entity_unit_ratio(dimensions["depth_unit"].ToString());
depth = dimensions["depth"].ToObject<float>() / ratio;
}
return (width, height, depth);
}
/* Function: get_set_texture
* ------------------
* Use unity web request to return a texture from the server.
*/
private IEnumerator get_set_texture(string url, Renderer renderer) {
UnityWebRequest www = UnityWebRequestTexture.GetTexture(base_url + url);
yield return www.SendWebRequest();
Texture texture = DownloadHandlerTexture.GetContent(www);
renderer.material.color = Color.white;
renderer.material.mainTexture = texture;
}
/* Function: upsert_entity
* ------------------
* Inserts or Updates an Entity using a combination of serialized and unserialized JSON
*
* The JToken entity_location contains an Entity object that has everything needed to
* make a new entity in the Unity scene.
*/
private void upsert_entity(string entity_id, JToken entity_location) {
// Take the frozen location data from DB and thaw it back into our class.
string serialized_location = entity_location["location"].ToString();
KuratorTransform update_transform = JsonUtility.FromJson<KuratorTransform>(serialized_location);
// Try to find the enity to Update.
GameObject entity = GameObject.Find(entity_id);
if (entity != null) {
// As we can't thaw straight to the transforms, we need to do a manual update.
Transform transform = entity.GetComponent<Transform>();
transform.position = update_transform.localPosition;
transform.rotation = update_transform.localRotation;
// Likely not needed for updates:
//transform.localScale = update_transform.localScale;
} else {
GameObject new_entity = create_blank_entity(entity_id);
Renderer renderer = new_entity.GetComponent<Renderer>();
new_entity.tag = "KuratorEntities";
string image_url = null;
foreach (JToken media_item in entity_location["entity"]["media"]) {
if (media_item["kind"].ToString() == "FULLRESOLUTION") {
image_url = media_item["url"].ToString();
}
}
renderer.material.color = Color.red;
if (image_url != null) {
StartCoroutine(get_set_texture(image_url, renderer));
}
// As we can't thaw straight to the transforms, we need to do a manual update.
Transform transform = new_entity.GetComponent<Transform>();
transform.position = update_transform.localPosition;
transform.rotation = update_transform.localRotation;
// TODO: Looks like we might not need to calculate the scale of the object after the first iteration
// as the data will already be serialized.
// transform.localScale = update_transform.localScale;
(float width, float height, float depth) = dimensions_to_unity_size(entity_location["entity"]["metadata"]["dimensions"]);
transform.localScale = new Vector3(width, height, 0);
}
}
/* Function: load_session
* ------------------
* Uses a GraphQL query 'session_by_id' to create or update session entities.
*/
public async void load_session() {
GraphApi.Query query = Graph.GetQueryByName("LoadSession", GraphApi.Query.Type.Query);
query.SetArgs(new{session_id = session_id});
UnityWebRequest request = await Graph.Post(query);
string response = System.Text.Encoding.UTF8.GetString(request.downloadHandler.data);
JObject jObject = JObject.Parse(response);
JToken entity_locations = jObject["data"]["session_by_id"]["entity_locations"];
foreach(JToken entity_location in entity_locations) {
string entity_id = entity_location["entity"]["entity_id"].ToString();
upsert_entity(entity_id, entity_location);
}
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using GraphQlClient.Core;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
[Serializable]
public class KuratorTransform {
public Vector3 localPosition;
public Quaternion localRotation;
public Vector3 localScale;
}
public class Mutations : MonoBehaviour
{
// Enable global access via Singleton.
public static Mutations m;
// Note that this is for application/graphql requests not application/json:
private string GQL_URL_GRAPHLQL = "https://kurator-core.herokuapp.com/api/graphql";
/* Function: Awake
* ------------------
* NOTE: Instantiate any Singletons on Awake.
* Then make sure everything that needs them runs
* on or afeter Start().
*/
void Awake() {
if (m == null) {
m = this;
} else if (m != this) {
Destroy(gameObject);
}
}
/* Function: gql_request
* ------------------
* This is a generic method that takes a Query (mutation/query) string
* a callback function of type Action<string> and sends it to the GQL
* server.
*
* It only returns a response string. GQL provides its own error handling
* for application level errors. Parse the string as JSON and see if it
* contains an error object.
*/
private IEnumerator gql_request(string request_string, Action<string> callback)
{
var request = new UnityWebRequest(GQL_URL_GRAPHLQL, "POST");
byte[] bodyRaw = Encoding.UTF8.GetBytes(request_string);
request.uploadHandler = (UploadHandler) new UploadHandlerRaw(bodyRaw);
request.downloadHandler = (DownloadHandler) new DownloadHandlerBuffer();
// Note that we are not sending JSON data for mutations:
request.SetRequestHeader("Content-Type", "application/graphql");
yield return request.SendWebRequest();
string response = Encoding.UTF8.GetString(request.downloadHandler.data);
callback(response);
}
/* Function: process_insert_device
* ------------------
* NOTE: We are now relying on KuratorState to store the device_id.
*/
private void process_insert_device(string response) {
JObject obj = JObject.Parse(response);
string device_id = obj["data"]["insert_device"]["device_id"].ToString();
KuratorState.v.device_id = device_id;
}
/* Function: insert_device_mutation
* ------------------
* Used on first start or complete reset. Identify the current device to the server.
* Ideally one per physical device.
*/
public void insert_device_mutation() {
string request = "mutation { insert_device { device_id } }";
StartCoroutine(gql_request(request, process_insert_device));
}
/* Function: process_insert_session
* ------------------
* The callback for insert_session.
* NOTE: We are now relying on KuratorState to store the session_id.
*/
private void process_insert_session(string response) {
JObject obj = JObject.Parse(response);
string session_id = obj["data"]["insert_session"]["session_id"].ToString();
KuratorState.v.session_id = session_id;
}
/* Function: insert_session_mutation
* ------------------
* Use device_id and collection_id to insert a new session.
* TODO: Extend with root_location?
*/
private void insert_session_mutation(string collection_id, string device_id) {
string request = "mutation { insert_session(collection_id: \"" + collection_id + "\", device_id: \"" + device_id + "\") { session_id } }";
StartCoroutine(gql_request(request, process_insert_session));
}
// NOTE: We are not depending on the KuratorState class for the device_id.
public void insert_session() {
insert_session_mutation(KuratorState.v.collection_id,KuratorState.v.device_id);
}
/* Function: process_update_session
* ------------------
* Can technically be a no op.
*/
private void process_update_session(string response) {
JObject obj = JObject.Parse(response);
Debug.Log(response);
string session_id = obj["data"]["update_session"]["session_id"].ToString();
// We don't need to update the session_id as it should be the same still. Can use this
// space to check for errors.
}
/* Function: update_session_mutation
* ------------------
* Build the mutation, call the generic GQL request with a callback.
* TODO: Extend with root_location?
*/
private void update_session_mutation(string session_id, List<string> _entities, List<string> _locations) {
string entities = new JArray(_entities).ToString();
string locations = new JArray(_locations).ToString();
string request = "mutation { update_session(session_id: \"" + session_id + "\", entities: " + entities + ", locations: " + locations + " ) { session_id } }";
StartCoroutine(gql_request(request, process_update_session));
}
/* Function: save_current_session
* ------------------
* Gather all entities on the specific layer.
* Convert them to a mutation string.
*
* NOTE: We are now relying on KuratorState for the session_id.
*/
public void save_current_session() {
List<string> entities_list = new List<string>();
List<string> locations_list = new List<string>();
// TODO: If this is not ideal, find another way to locate all the current entities.
GameObject[] entity_objects = GameObject.FindGameObjectsWithTag("KuratorEntities");
foreach (GameObject entity in entity_objects) {
Transform transform = entity.GetComponent<Transform>();
KuratorTransform kTransform = new KuratorTransform();
kTransform.localPosition = transform.position;
kTransform.localRotation = transform.rotation;
kTransform.localScale = transform.localScale;
string json_transform = JsonUtility.ToJson(kTransform);
entities_list.Add(entity.name);
locations_list.Add(json_transform);
}
update_session_mutation(KuratorState.v.session_id, entities_list, locations_list);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment