diff --git a/Assets/SEE/Controls/Actions/ActionStateType.cs b/Assets/SEE/Controls/Actions/ActionStateType.cs index 10cacd6db2..345e60967c 100644 --- a/Assets/SEE/Controls/Actions/ActionStateType.cs +++ b/Assets/SEE/Controls/Actions/ActionStateType.cs @@ -62,6 +62,10 @@ public class ActionStateType new ActionStateType(9, "Draw", "Draw a line", Color.magenta.Darker(), "Materials/ModernUIPack/Pencil", DrawAction.CreateReversibleAction); + public static ActionStateType Mark { get; } = + new ActionStateType(10, "Mark", "Mark a Node with a hovering sphere", + Color.magenta.Darker(), "Materials/40+ Simple Icons - Free/Purpose_Simple_Icons_UI", + MarkAction.CreateReversibleAction); #endregion /// diff --git a/Assets/SEE/Controls/Actions/MarkAction.cs b/Assets/SEE/Controls/Actions/MarkAction.cs new file mode 100644 index 0000000000..6bb9f03961 --- /dev/null +++ b/Assets/SEE/Controls/Actions/MarkAction.cs @@ -0,0 +1,165 @@ +using System.Collections.Generic; +using SEE.Game; +using SEE.GO; +using SEE.Net; +using SEE.Utils; +using UnityEngine; + +namespace SEE.Controls.Actions +{ + /// + /// Action to toggle a marking for a node. + /// The mark is a white sphere, hovering over the node. + /// + internal class MarkAction : AbstractPlayerAction + { + /// + /// If the user clicks with the mouse hitting a game object representing a node, + /// this node gets a mark. + /// . + /// + /// true if completed + public override bool Update() + { + bool result = false; + + // FIXME: Needs adaptation for VR where no mouse is available. + if (Input.GetMouseButtonDown(0) + && Raycasting.RaycastGraphElement(out RaycastHit raycastHit, out GraphElementRef _) == HitGraphElement.Node) + { + /// the hit object is the Node which gets a mark as a child. + GameObject parent = raycastHit.collider.gameObject; + /// the position of the Node is used for the mark. + Vector3 position = parent.transform.position; + /// the scale of the Node is used to make the Sphere fit into the ground space of the node. + Vector3 scale = FindSize(parent); + GameNodeMarker.Mark(parent, position: position, worldSpaceScale: scale); + memento = new Memento(parent, position: position, scale: scale); + new MarkNetAction(parentID: memento.Parent.name, memento.Position, memento.Scale).Execute(); + result = true; + currentState = ReversibleAction.Progress.Completed; + } + return result; + } + + /// + /// Memento capturing the data necessary to re-do this action. + /// + private Memento memento; + + /// + /// The information we need to re-add a mark whose addition was undone. + /// + private struct Memento + { + /// + /// The node which is marked. + /// + public readonly GameObject Parent; + /// + /// The position of the mark in world space. + /// + public readonly Vector3 Position; + /// + /// The scale of the new mark in world space. + /// + public readonly Vector3 Scale; + + /// + /// Constructor setting the information necessary to re-do this action. + /// + /// The node which is marked. + /// position of the mark in world space. + /// scale of the mark in world space. + public Memento(GameObject parent, Vector3 position, Vector3 scale) + { + Parent = parent; + Position = position; + Scale = scale; + } + } + + /// + /// Returns a scale of a cube that fits into the ground area of . + /// + /// parent in which ground area to fit the cube. + /// the scale of a cube that fits into the ground area of . + private static Vector3 FindSize(GameObject parent) + { + Vector3 result = parent.transform.lossyScale; + /// The ground area of the result must be a square. + if (result.x > result.z) + { + result.x = result.z; + } + else + { + result.z = result.x; + } + /// make the square a cube. + result.y = result.z; + return result; + } + + /// + /// Undoes this MarkAction. + /// + public override void Undo() + { + base.Undo(); + + GameNodeMarker.Mark(memento.Parent, position: memento.Position, worldSpaceScale: memento.Scale); + new MarkNetAction(parentID: memento.Parent.name, memento.Position, memento.Scale).Execute(); + + } + + /// + /// Redoes this MarkAction. + /// + public override void Redo() + { + base.Redo(); + GameNodeMarker.Mark(memento.Parent, position: memento.Position, worldSpaceScale: memento.Scale); + new MarkNetAction(parentID: memento.Parent.name, memento.Position, memento.Scale).Execute(); + } + + /// + /// Returns a new instance of . + /// new instance + public static ReversibleAction CreateReversibleAction() + { + return new MarkAction(); + } + + /// + /// Returns a new instance of . + /// + /// new instance + public override ReversibleAction NewInstance() + { + return CreateReversibleAction(); + } + + /// + /// Returns the of this action. + /// + /// + public override ActionStateType GetActionStateType() + { + return ActionStateType.Mark; + } + + /// + /// Returns all IDs of gameObjects manipulated by this action. + /// + /// all IDs of gameObjects manipulated by this action + public override HashSet GetChangedObjects() + { + return new HashSet + { + memento.Parent.name + }; + } + } +} \ No newline at end of file diff --git a/Assets/SEE/Controls/Actions/MarkAction.cs.meta b/Assets/SEE/Controls/Actions/MarkAction.cs.meta new file mode 100644 index 0000000000..9b22de1f6d --- /dev/null +++ b/Assets/SEE/Controls/Actions/MarkAction.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0a8cbb4ba5a85514e95f72f80e62bf06 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SEE/DataModel/DG/Node.cs b/Assets/SEE/DataModel/DG/Node.cs index a41f452f84..08319fc172 100644 --- a/Assets/SEE/DataModel/DG/Node.cs +++ b/Assets/SEE/DataModel/DG/Node.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using UnityEngine; namespace SEE.DataModel.DG { @@ -163,10 +164,38 @@ private set } /// - /// True iff node has no parent. + /// Is true, if the node is marked /// - /// true iff node is a root node - public bool IsRoot() + private bool isMarked = false; + + /// + /// Is true, if the node is marked + /// + public bool IsMarked + { + get => isMarked; + set => isMarked = value; + } + + /// + /// The GameObject with which the node was marked + /// + private GameObject marking; + + /// + /// The GameObject with which the node was marked + /// + public GameObject Marking + { + get => marking; + set => marking = value; + } + + /// + /// True iff node has no parent. + /// + /// true iff node is a root node + public bool IsRoot() { return Parent == null; } diff --git a/Assets/SEE/Game/GameNodeMarker.cs b/Assets/SEE/Game/GameNodeMarker.cs new file mode 100644 index 0000000000..672d02b4e0 --- /dev/null +++ b/Assets/SEE/Game/GameNodeMarker.cs @@ -0,0 +1,70 @@ +using System; +using SEE.DataModel.DG; +using SEE.Game.City; +using SEE.GO; +using UnityEngine; + +namespace SEE.Game +{ + /// + /// Toggle a marking for a node. + /// The mark is a white sphere, hovering over the node. + /// + public static class GameNodeMarker + { + + /// + /// Toggles the marking of a node at the + /// given with the given . + /// The marking is a new GameObject, added as a child of the Node. + /// The GameObject is represented by a white hovering sphere above the node which is marked. + /// + /// Precondition: must have a valid node reference. + /// + /// node which marking will be toggled. + /// the position in world space for the center point of the mark. + /// the scale in world space of the mark. + /// thrown if is not contained in a code city. + public static void Mark(GameObject parent, Vector3 position, Vector3 worldSpaceScale) + { + SEECity city = parent.ContainingCity() as SEECity; + if (city != null) + { + /// Gets the actual GameObject which represents the node. + Node parentNode = parent.GetNode(); + /// Mark if the node is not marked. + if (!parentNode.IsMarked) { + GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); + sphere.transform.localScale = worldSpaceScale; + /// The sphere is located at the golden ratio with the smaller part being the distance to the sphere, and the bigger part being the sphere. + /// Mathematical explanation of the position: + /// parent.transform.position.y = middle of node. + /// parent.transform.lossyScale.y/2 = half of the height of the node. + /// worldSpaceScale.y*0.5 = half of the height of the sphere. + /// because the position of the sphere describes the middle of itself, we add all 3 values together, to put the mark ontop of the node. + /// We finally add worldSpaceScale.y*0.38196601125F to get the smaller part of the golden ratio in relation to the size of the sphere as distance to the node. + /// Because "worldSpaceScale.y*0.5 + worldSpaceScale.y*0.38196601125F" is the same result as "worldSpaceScale.y*0.88196601125F", we simplify it. + sphere.transform.position = new Vector3(position.x, parent.transform.position.y + parent.transform.lossyScale.y/2 + worldSpaceScale.y* 0.88196601125F, position.z); + sphere.transform.SetParent(parent.transform); + Portal.SetPortal(city.gameObject, gameObject: sphere); + parentNode.IsMarked = true; + parentNode.Marking = sphere; + return; + } + /// unmark if the node is marked. + else + { + parentNode.IsMarked = false; + GameObject marking = parentNode.Marking; + GameObject.Destroy(marking); + return; + } + + } + else + { + throw new Exception($"The node {parent.name} is not contained in a code city."); + } + } + } +} diff --git a/Assets/SEE/Game/GameNodeMarker.cs.meta b/Assets/SEE/Game/GameNodeMarker.cs.meta new file mode 100644 index 0000000000..8e12579b38 --- /dev/null +++ b/Assets/SEE/Game/GameNodeMarker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9159bf7682c3e9d47ae67bd910089581 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SEE/Net/Actions/MarkNetAction.cs b/Assets/SEE/Net/Actions/MarkNetAction.cs new file mode 100644 index 0000000000..1be4026564 --- /dev/null +++ b/Assets/SEE/Net/Actions/MarkNetAction.cs @@ -0,0 +1,72 @@ +using SEE.Game; +using UnityEngine; + +namespace SEE.Net +{ + /// + /// This class is responsible for marking a node via network from one client to all others and + /// to the server. + /// + public class MarkNetAction : AbstractNetAction + { + // Note: All attributes are made public so that they will be serialized + // for the network transfer. + + /// + /// The node which is marked. + /// + public string ParentID; + + /// + /// The position of the mark in world space. + /// + public Vector3 Position; + + /// + /// The scale of the new mark in world space. + /// + public Vector3 Scale; + + /// + /// Constructor. + /// + /// unique ID of the Node which will get marked. + /// the position for the mark. + /// the scale of the mark in world space. + public MarkNetAction + (string parentID, + Vector3 position, + Vector3 scale) + : base() + { + this.ParentID = parentID; + this.Position = position; + this.Scale = scale; + } + + /// + /// Things to execute on the server (none for this class). Necessary because it is abstract + /// in the superclass. + /// + protected override void ExecuteOnServer() + { + // Intentionally left blank. + } + + /// + /// Toggle the marking of the node on each client. + /// + protected override void ExecuteOnClient() + { + if (!IsRequester()) + { + GameObject parent = GraphElementIDMap.Find(ParentID); + if (parent == null) + { + throw new System.Exception($"There is no node with the ID {ParentID}."); + } + GameNodeMarker.Mark(parent, Position, Scale); + } + } + } +} diff --git a/Assets/SEE/Net/Actions/MarkNetAction.cs.meta b/Assets/SEE/Net/Actions/MarkNetAction.cs.meta new file mode 100644 index 0000000000..253c1989f6 --- /dev/null +++ b/Assets/SEE/Net/Actions/MarkNetAction.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ffc3f979740dc524a9948e012ee5d3e5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: