#if UNITY_STANDALONE || UNITY_EDITOR using UnityEngine; using System.Collections; using ProBuilder2.Common; namespace ProBuilder2.Examples { /** * \brief This class allows the user to select a single face at a time and move it forwards or backwards. * More advanced usage of the ProBuilder API should make use of the pb_Object->SelectedFaces list to keep * track of the selected faces. */ public class RuntimeEdit : MonoBehaviour { class pb_Selection { public pb_Object pb; ///< This is the currently selected ProBuilder object. public pb_Face face; ///< Keep a reference to the currently selected face. public pb_Selection(pb_Object _pb, pb_Face _face) { pb = _pb; face = _face; } public bool HasObject() { return pb != null; } public bool IsValid() { return pb != null && face != null; } public bool Equals(pb_Selection sel) { if(sel != null && sel.IsValid()) return (pb == sel.pb && face == sel.face); else return false; } public void Destroy() { if(pb != null) GameObject.Destroy(pb.gameObject); } public override string ToString() { return "pb_Object: " + pb == null ? "Null" : pb.name + "\npb_Face: " + ( (face == null) ? "Null" : face.ToString() ); } } pb_Selection currentSelection; pb_Selection previousSelection; private pb_Object preview; public Material previewMaterial; /** * \brief Wake up! */ void Awake() { SpawnCube(); } /** * \brief This is the usual Unity OnGUI method. We only use it to show a 'Reset' button. */ void OnGUI() { // To reset, nuke the pb_Object and build a new one. if(GUI.Button(new Rect(5, Screen.height - 25, 80, 20), "Reset")) { currentSelection.Destroy(); Destroy(preview.gameObject); SpawnCube(); } } /** * \brief Creates a new ProBuilder cube and sets it up with a concave MeshCollider. */ void SpawnCube() { // This creates a basic cube with ProBuilder features enabled. See the ProBuilder.Shape enum to // see all possible primitive types. pb_Object pb = pb_ShapeGenerator.CubeGenerator(Vector3.one); // The runtime component requires that a concave mesh collider be present in order for face selection // to work. pb.gameObject.AddComponent().convex = false; // Now set it to the currentSelection currentSelection = new pb_Selection(pb, null); } Vector2 mousePosition_initial = Vector2.zero; bool dragging = false; public float rotateSpeed = 100f; /** * \brief This is responsible for moving the camera around and not much else. */ public void LateUpdate() { if(!currentSelection.HasObject()) return; if(Input.GetMouseButtonDown(1) || (Input.GetMouseButtonDown(0) && Input.GetKey(KeyCode.LeftAlt))) { mousePosition_initial = Input.mousePosition; dragging = true; } if(dragging) { Vector2 delta = (Vector3)mousePosition_initial - (Vector3)Input.mousePosition; Vector3 dir = new Vector3(delta.y, delta.x, 0f); currentSelection.pb.gameObject.transform.RotateAround(Vector3.zero, dir, rotateSpeed * Time.deltaTime); // If there is a currently selected face, update the preview. if(currentSelection.IsValid()) RefreshSelectedFacePreview(); } if(Input.GetMouseButtonUp(1) || Input.GetMouseButtonUp(0)) { dragging = false; } } /** * \brief The 'meat' of the operation. This listens for a click event, then checks for a positive * face selection. If the click has hit a pb_Object, select it. */ public void Update() { if(Input.GetMouseButtonUp(0) && !Input.GetKey(KeyCode.LeftAlt)) { if(FaceCheck(Input.mousePosition)) { if(currentSelection.IsValid()) { // Check if this face has been previously selected, and if so, move the face. // Otherwise, just accept this click as a selection. if(!currentSelection.Equals(previousSelection)) { previousSelection = new pb_Selection(currentSelection.pb, currentSelection.face); RefreshSelectedFacePreview(); return; } Vector3 localNormal = pb_Math.Normal( pbUtil.ValuesWithIndices(currentSelection.pb.vertices, currentSelection.face.distinctIndices) ); if(Input.GetKey(KeyCode.LeftShift)) currentSelection.pb.TranslateVertices( currentSelection.face.distinctIndices, localNormal.normalized * -.5f ); else currentSelection.pb.TranslateVertices( currentSelection.face.distinctIndices, localNormal.normalized * .5f ); // Refresh will update the Collision mesh volume, face UVs as applicatble, and normal information. currentSelection.pb.Refresh(); // this create the selected face preview RefreshSelectedFacePreview(); } } } } /** * \brief This is how we figure out what face is clicked. */ public bool FaceCheck(Vector3 pos) { Ray ray = Camera.main.ScreenPointToRay (pos); RaycastHit hit; if( Physics.Raycast(ray.origin, ray.direction, out hit)) { pb_Object hitpb = hit.transform.gameObject.GetComponent(); if(hitpb == null) return false; Mesh m = hitpb.msh; int[] tri = new int[3] { m.triangles[hit.triangleIndex * 3 + 0], m.triangles[hit.triangleIndex * 3 + 1], m.triangles[hit.triangleIndex * 3 + 2] }; currentSelection.pb = hitpb; return hitpb.FaceWithTriangle(tri, out currentSelection.face); } return false; } void RefreshSelectedFacePreview() { // Copy the currently selected vertices in world space. // World space so that we don't have to apply transforms // to match the current selection. Vector3[] verts = currentSelection.pb.VerticesInWorldSpace(currentSelection.face.indices); // face.indices == triangles, so wind the face to match int[] indices = new int[verts.Length]; for(int i = 0; i < indices.Length; i++) indices[i] = i; // Now go through and move the verts we just grabbed out about .1m from the original face. Vector3 normal = pb_Math.Normal(verts); for(int i = 0; i < verts.Length; i++) verts[i] += normal.normalized * .01f; if(preview) Destroy(preview.gameObject); preview = pb_Object.CreateInstanceWithVerticesFaces(verts, new pb_Face[] { new pb_Face(indices) }); preview.SetFaceMaterial(preview.faces, previewMaterial); preview.ToMesh(); preview.Refresh(); } } } #endif