Line snapping for measurement functionality

Hi all!

I am trying to create a simple measurement script that will measure flat surfaces straight across. Currently, I am able to click left to declare a start point, and then hold down right and drag my line renderer around until I let go to see the length of that line.

However, I would like to only be able to measure at 90 degree intervals from my start point, similar to a snapping feature so the lines only render directly up, down, left, or right in order to get the most accurate readings.


Here is a visualization of how I would like it to work and my code so far. Appreciate any kind of help I can get as I have kind of hit a wall.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class RigidMeasure : MonoBehaviour
{
    public GameObject laserPrefab;
    private GameObject laser;
    private Transform laserTransform;
    public GameObject marker;

    private int count = 0;
    private float dist;
    private string final;
    private float angle;

    private Vector3 point1;
    private Vector3 point2;

    // Start is called before the first frame update
    void Start()
    {
        laser = Instantiate(laserPrefab);   //creates an instance of the laser prefab
        laserTransform = laser.transform;   //defines the laser transform
        laser.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        PlacePoint1();
        PlacePoint2();

        ClearMeasurement();
    }

    void PlacePoint1()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Physics.Raycast(ray, out RaycastHit hit, 100.0f);

        if (Input.GetMouseButtonDown(0) && count == 0)
        {
            Instantiate(marker, hit.point, Quaternion.identity);
            point1 = hit.point;
            count++;
        }
    }

    void PlacePoint2()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Physics.Raycast(ray, out RaycastHit hit, 100.0f);

        if (Input.GetMouseButton(1) && count == 1)
        {
            point2 = hit.point;
            DrawLaser();
        }

        if (Input.GetMouseButtonUp(1) && count == 1)
        {
            Instantiate(marker, hit.point, Quaternion.identity);
            GeneratePrefabText();
            count++;
            AngleBetweenVector2(point1, point2);
        }
    }

    void DrawLaser()
    {
        //If both points are declared
        laser.SetActive(true);  // Makes laser visible

        Vector3 v1 = point1;
        Vector3 v2 = point2;

        // Place, orient and stretch measurement laser
        laserTransform.position = Vector3.Lerp(v1, v2, .5f);
        laserTransform.LookAt(v2);
        dist = Vector3.Distance(v1, v2);
        final = dist.ToString();
        laserTransform.localScale = new Vector3(laserTransform.localScale.x, laserTransform.localScale.y, dist);

        // Convert metric to imperial
        float inches = dist * 39.36f;
        int feet = 0;
        bool end = false;

        // Subtracts foot from total until remaining inches are found
        while (end == false)
        {
            if (inches < 12)
            {
                end = true;

                final = feet.ToString() + "'" + Math.Floor(inches).ToString() + '"';
            }
            else
            {
                feet++;
                inches -= 12;
            }
        }
    }

    //creates a gameobject consisting of the correct number measurements from prefabs
    void GeneratePrefabText()
    {
        GameObject letterCanvas = new GameObject();
        letterCanvas.name = "NumberCanvas";
        float offset = 0f;
        foreach (char c in final)
        {
            string number;
            GameObject character;
            //converts the char that is read into a string that can be used to search the Resources/Numbers/Prefabs folder
            if (c == '"')
            {
                number = "quote";
            }
            else if (c == '\'')
            {
                number = "apostrophe";
            }
            else
            {
                number = c.ToString();
            }
            //correctly instantiates each prefab and makes them all children of a canvas
            character = Instantiate(Resources.Load("Numbers/Prefabs/" + number, typeof(GameObject)) as GameObject);
            character.transform.position = new Vector3(offset, 4f, 0f);
            character.transform.localScale = new Vector3(2f, 2f, 2f);
            character.transform.Rotate(0, 180, 0);
            character.transform.SetParent(letterCanvas.transform);
            offset += 0.6f;
        }
    }

    void ClearMeasurement()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            SceneManager.LoadScene("MeasurementTest");
        }
    }

    private float AngleBetweenVector2(Vector2 vec1, Vector2 vec2)
    {
        Vector2 diference = vec2 - vec1;
        float sign = (vec2.y < vec1.y) ? -1.0f : 1.0f;
        Debug.Log(Vector2.Angle(Vector2.right, diference) * sign);
        angle = Vector2.Angle(Vector2.right, diference) * sign;
        return Vector2.Angle(Vector2.right, diference) * sign;
    }

    void FindAngleToSnap()
    {
        if ((AngleBetweenVector2(point1, point2) > -45f) && (AngleBetweenVector2(point1, point2) < 45f))
        {
            angle = 0;
        }
        else if ((AngleBetweenVector2(point1, point2) > 45f) && (AngleBetweenVector2(point1, point2) < 135f))
        {
            angle = 90;
        }
        else if ((AngleBetweenVector2(point1, point2) > 135f) && (AngleBetweenVector2(point1, point2) < -135f))
        {
            angle = 180;
        }
        else
        {
            angle = -90;
        }
    }
}

Without searching for it in the above code, let me suggest this:

At some point you have a Vector2 (or Vector3) offset of the line in progress.

Instead of just using it, force it to ONLY be on the longest axis, and zero on the other.

Vector2 MyOffset = .... (however you get this)

// if x is absolutely larger than y, zero out y, and vice-versa
if (Mathf.Abs( MyOffset.x) > Mathf.Abs( MyOffset.y))
{
  MyOffset.y = 0;
}
else
{
  MyOffset.x = 0;
}

Then use the offset.

How do I use the offset to instantiate my laser prefab correctly?

Looking above, isn’t the offset quantity just the difference between point1 and point2?

yes, I think I figured it out by doing this since my first point can be set anywhere

// if x is absolutely larger than y, set v2.y to v1.y, and vice-versa
        if (Mathf.Abs(point2.x) > Mathf.Abs(point2.y))
        {
            point2.y = point1.y;
        }
        else
        {
            point2.x = point1.x;
        }
1 Like

Would there be a somewhat simple way to convert this same idea to snap at 45 degree intervals?

Yes! Slightly-different conditionals and then just assign the other quantity when diagonal.

To get the conditionals right you need to ask if it is within the axis-aligned cones, or the diagonal aligned codes.

A more general approach is to get the radial heading (with Mathf.Atan2() ), quantize that heading to some number of discrete steps around the circle (in this case 8), then reproduce the offsets using Mathf.Sin()/Mathf.Cos().

Remember those functions all operate on radians, not degrees. You can convert back and forth with Mathf.Rad2Deg / Deg2Rad

So I’m getting a little stuck still. I have been able to snap to all the angles necessary. However, I do not know how to draw my line as long as I want when I snap to the new angles.

if (AngleBetweenVector2(point1, point2) > 22.5 && AngleBetweenVector2(point1, point2) < 67.5)            //45
        {
            point2.y = point1.y + 1;
            point2.x = point1.x + 1;
        }
        else if (AngleBetweenVector2(point1, point2) > 67.5 && AngleBetweenVector2(point1, point2) < 112.5)        //90
        {
            point2.x = point1.x;
        }
        else if (AngleBetweenVector2(point1, point2) > 112.5 && AngleBetweenVector2(point1, point2) < 157.5)    //135
        {
            point2.y = point1.y + 1;
            point2.x = point1.x - 1;
        }
        else if (AngleBetweenVector2(point1, point2) > 157.5 || AngleBetweenVector2(point1, point2) < -157.5)    //180
        {
            point2.y = point1.y;
        }
        else if (AngleBetweenVector2(point1, point2) > -157.5 && AngleBetweenVector2(point1, point2) < -112.5)        //-135
        {
            point2.y = point1.y - 1;
            point2.x = point1.x - 1;
        }
        else if (AngleBetweenVector2(point1, point2) > -112.5 && AngleBetweenVector2(point1, point2) < -67.5)     //-90
        {
            point2.x = point1.x;
        }
        else if (AngleBetweenVector2(point1, point2) > -67.5 && AngleBetweenVector2(point1, point2) < -22.5)    //-45
        {
            point2.y = point1.y - 1;
            point2.x = point1.x + 1;
        }
        else if (AngleBetweenVector2(point1, point2) > -22.5 && AngleBetweenVector2(point1, point2) < 22.5)   //0
        {
            point2.y = point1.y;
        }
private float AngleBetweenVector2(Vector2 vec1, Vector2 vec2)
    {
        Vector2 diference = vec2 - vec1;
        float sign = (vec2.y < vec1.y) ? -1.0f : 1.0f;
        Debug.Log(Vector2.Angle(Vector2.right, diference) * sign);
        angle = Vector2.Angle(Vector2.right, diference) * sign;
        return angle;
    }

I found my demo package for quantizing inputs… this is the same process you would need. This takes an arbitrary analog 2D vector and makes sure it ONLY exists in discrete slices, in this case 8. But it is the general case solution so you could say “I want 16 possible angles around the circle” or whatever.

See full package, run the scene and see the code.

7395671–903254–QuantizedAngles.unitypackage (4.79 KB)

@Kurt-Dekker I am struggling figuring out how to implement this into my class. I have created a function that will find the angle of the line that is drawn.

private float AngleBetweenVector2(Vector2 vec1, Vector2 vec2)
    {
        Vector2 diference = vec2 - vec1;
        float sign = (vec2.y < vec1.y) ? -1.0f : 1.0f;
        //Debug.Log(Vector2.Angle(Vector2.right, diference) * sign);
        angle = Vector2.Angle(Vector2.right, diference) * sign;
        return angle;
    }

However, I still need to find a way to correctly adjust the magnitude of the line after it snaps. Right now, if the first point is placed in the center of my box, everything works fairly well. But if the first point is closer to the sides, the diagonals only show on 1 side. For example, if I place point 1 close to the right edge of my box, the 135 and -135 degree angles show up at -45 and 45 degrees respectfully.

public GameObject laserPrefab;
    private GameObject laser;
    private Transform laserTransform;
    public GameObject marker;

    private int count = 0;
    private float dist;
    private string final;
    private float angle;
    public int NumAngles = 8;

    public Vector3 point1;
    public Vector3 point2;
    public Vector3 worldPosition;

    // Start is called before the first frame update
    void Start()
    {
        laser = Instantiate(laserPrefab);   //creates an instance of the laser prefab
        laserTransform = laser.transform;   //defines the laser transform
        laser.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        Vector3 mousePos = Input.mousePosition;
        mousePos.z = Camera.main.nearClipPlane;
        worldPosition = Camera.main.ScreenToWorldPoint(mousePos);

        PlacePoint1();
        PlacePoint2();

        ClearMeasurement();
    }

    void PlacePoint1()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Physics.Raycast(ray, out RaycastHit hit, 100.0f);

        if (Input.GetMouseButtonDown(0) && count == 0)
        {
            Instantiate(marker, hit.point, Quaternion.identity);
            point1 = hit.point;
            count++;
            Debug.Log(worldPosition);
        }
    }

    void PlacePoint2()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Physics.Raycast(ray, out RaycastHit hit, 100.0f);

        if (Input.GetMouseButton(1) && count == 1)
        {
            point2 = hit.point;
            DrawLaser();
        }

        if (Input.GetMouseButtonUp(1) && count == 1)
        {
            //Instantiate(marker, hit.point, Quaternion.identity);
            GeneratePrefabText();
            count++;
            AngleBetweenVector2(point1, point2);
            Instantiate(marker, point2, Quaternion.identity);
        }
    }

    void DrawLaser()
    {
        //If both points are declared
        laser.SetActive(true);  // Makes laser visible

        //-0.3 to 0.3 x
        //0.9 to 1.1 y
        if (AngleBetweenVector2(point1, point2) > 22.5 && AngleBetweenVector2(point1, point2) < 67.5)            //45
        {
            point2.y = point1.y + worldPosition.x * 30;
            point2.x = point1.x + worldPosition.x * 30;
            //point2 = Quaternion.Euler(newRot);
        }
        else if (AngleBetweenVector2(point1, point2) > 67.5 && AngleBetweenVector2(point1, point2) < 112.5)        //90
        {
            point2.x = point1.x;
        }
        else if (AngleBetweenVector2(point1, point2) > 112.5 && AngleBetweenVector2(point1, point2) < 157.5)    //135
        {
            point2.y = point1.y - worldPosition.x * 30;
            point2.x = point1.x + worldPosition.x * 30;
        }
        else if (AngleBetweenVector2(point1, point2) > 157.5 || AngleBetweenVector2(point1, point2) < -157.5)    //180
        {
            point2.y = point1.y;
        }
        else if (AngleBetweenVector2(point1, point2) > -157.5 && AngleBetweenVector2(point1, point2) < -112.5)    //-135
        {
            point2.y = point1.y + worldPosition.x * 30;
            point2.x = point1.x + worldPosition.x * 30;
        }
        else if (AngleBetweenVector2(point1, point2) > -112.5 && AngleBetweenVector2(point1, point2) < -67.5)    //-90
        {
            point2.x = point1.x;
        }
        else if (AngleBetweenVector2(point1, point2) > -67.5 && AngleBetweenVector2(point1, point2) < -22.5)    //-45
        {
            point2.y = point1.y - worldPosition.x * 30;
            point2.x = point1.x + worldPosition.x * 30;
        }
        else if (AngleBetweenVector2(point1, point2) > -22.5 && AngleBetweenVector2(point1, point2) < 22.5)        //0
        {
            point2.y = point1.y;
        }

        // Place, orient and stretch measurement laser
        laserTransform.position = Vector3.Lerp(point1, point2, .5f);
        laserTransform.LookAt(point2);
        dist = Vector3.Distance(point1, point2);
        final = dist.ToString();
        laserTransform.localScale = new Vector3(laserTransform.localScale.x, laserTransform.localScale.y, dist);

        // Convert metric to imperial
        float inches = dist * 39.36f;
        int feet = 0;
        bool end = false;

        // Subtracts foot from total until remaining inches are found
        while (end == false)
        {
            if (inches < 12)
            {
                end = true;

                final = feet.ToString() + "'" + Math.Floor(inches).ToString() + '"';
            }
            else
            {
                feet++;
                inches -= 12;
            }
        }
    }