Last active
June 2, 2020 08:37
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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