diff --git a/Assets/Scripts/Anchor.meta b/Assets/Scripts/Anchor.meta
new file mode 100644
index 0000000..13cb4bc
--- /dev/null
+++ b/Assets/Scripts/Anchor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 60cc4243135e5cd4b9d21f837db97b31
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Anchor/Anchor.cs b/Assets/Scripts/Anchor/Anchor.cs
new file mode 100644
index 0000000..14b8687
--- /dev/null
+++ b/Assets/Scripts/Anchor/Anchor.cs
@@ -0,0 +1,63 @@
+using UnityEngine;
+#if UNITY_EDITOR
+using UnityEditor;
+#endif
+
+public class Anchor : MonoBehaviour
+{
+ // Comparison tolerances
+ const float positionEPS = 0.1f;
+ const float rotationEPS = 0.1f;
+
+ // See anchor within editor
+ #if UNITY_EDITOR
+ public const float tipsSize = 0.2f;
+
+ private void OnDrawGizmosSelected()
+ {
+ Handles.color = Color.magenta;
+ Vector3 position = transform.position;
+ float size = transform.lossyScale.x;
+
+ if (size >= 0)
+ {
+ Handles.ArrowHandleCap(
+ 0,
+ position,
+ Quaternion.LookRotation(transform.forward),
+ size,
+ EventType.Repaint
+ );
+
+ Vector3 leftLimit = position - size*transform.right;
+ Vector3 rightLimit = position + size*transform.right;
+ Handles.DrawLine(leftLimit, rightLimit);
+
+ Handles.DrawLine(
+ leftLimit - tipsSize*size*transform.forward,
+ leftLimit + tipsSize*size*transform.forward
+ );
+ Handles.DrawLine(
+ rightLimit - tipsSize*size*transform.forward,
+ rightLimit + tipsSize*size*transform.forward
+ );
+ }
+ }
+ #endif
+
+ ///
+ /// Test whether this anchor is connected to another anchor
+ ///
+ /// The other anchor to compare to
+ /// Whether to test for scale match
+ /// The anchoring state of this anchor and the given anchor
+ public bool IsAnchoredTo(Anchor other, bool matchScales=true)
+ {
+ if (matchScales && (Mathf.Abs(transform.lossyScale.x - other.transform.lossyScale.x) > positionEPS))
+ {
+ return false;
+ }
+ return Quaternion.Angle(transform.rotation*Quaternion.AngleAxis(180f, Vector3.up), other.transform.rotation) < rotationEPS
+ && Vector3.Distance(transform.position, other.transform.position) < positionEPS;
+ }
+}
diff --git a/Assets/Scripts/Anchor/Anchor.cs.meta b/Assets/Scripts/Anchor/Anchor.cs.meta
new file mode 100644
index 0000000..670587e
--- /dev/null
+++ b/Assets/Scripts/Anchor/Anchor.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c8e92a0688e18aa4891eb7937ca9cd0e
\ No newline at end of file
diff --git a/Assets/Scripts/Anchor/AnchoredObject.cs b/Assets/Scripts/Anchor/AnchoredObject.cs
new file mode 100644
index 0000000..decbef7
--- /dev/null
+++ b/Assets/Scripts/Anchor/AnchoredObject.cs
@@ -0,0 +1,120 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+public class AnchoredObject : MonoBehaviour
+{
+ [SerializeField] List anchors = new List();
+
+ ///
+ /// Get an anchor from its ID (index)
+ ///
+ /// ID of the anchor to search for
+ /// The anchor that has the given ID
+ public Anchor GetAnchorFromID(int anchorID)
+ {
+ if (anchorID > anchors.Count)
+ {
+ Debug.LogError("Error: Tried to anchor to an inexisting anchor. Check anchors list.");
+ }
+ return anchors[anchorID];
+ }
+
+ ///
+ /// Anchor this object to another
+ ///
+ /// The anchored object to connect to
+ /// ID of the partner anchor to connect to
+ /// ID of anchor this object wants to connect
+ /// Whether to anchor scales too
+ public void AnchorTo(AnchoredObject partner, int partnerAnchorID, int myAnchorID, bool matchScales=true)
+ {
+ AnchorTo(
+ GetAnchorFromID(myAnchorID),
+ partner.GetAnchorFromID(partnerAnchorID)
+ );
+ }
+
+ ///
+ /// Anchor this object to another
+ ///
+ /// The anchor to connect
+ /// The anchor of the other object to connect to
+ /// Whether to anchor scales too
+ public void AnchorTo(Anchor myAnchor, Anchor partnerAnchor, bool matchScales=true)
+ {
+ if (matchScales)
+ {
+ // Match scales
+ float sizeDelta = partnerAnchor.transform.lossyScale.x / myAnchor.transform.lossyScale.x;
+ transform.localScale *= sizeDelta;
+ }
+
+ // Match rotations
+ transform.rotation =
+ partnerAnchor.transform.rotation
+ * Quaternion.AngleAxis(180f, Vector3.up)
+ * Quaternion.Inverse(myAnchor.transform.rotation)
+ * transform.rotation;
+
+ // Match positions
+ transform.position += partnerAnchor.transform.position - myAnchor.transform.position;
+ }
+
+ ///
+ /// Connect multiple anchors of this object with multiple anchors.
+ /// If one of the connections fails, the anchoring operation fails.
+ ///
+ /// The anchored object to connect to
+ /// IDs of the partner anchors to connect to
+ /// IDs of the anchors this object wants to connect
+ /// Whether to anchor scales too
+ /// Whether the anchoring succeeded or not
+ public bool AnchorToMultiple(AnchoredObject partner, IList partnerAnchorIDs, IList myAnchorIDs, bool matchScales=true)
+ {
+ if (myAnchorIDs.Count != partnerAnchorIDs.Count)
+ {
+ Debug.LogError("Error: Number of anchors must match.");
+ }
+ List myAnchors = new List();
+ List partnerAnchors = new List();
+ for (int i = 0; i < myAnchorIDs.Count; i++)
+ {
+ myAnchors.Add(GetAnchorFromID(myAnchorIDs[i]));
+ partnerAnchors.Add(partner.GetAnchorFromID(partnerAnchorIDs[i]));
+ }
+ return AnchorToMultiple(myAnchors, partnerAnchors, matchScales);
+ }
+
+ ///
+ /// Connect multiple anchors of this object with multiple anchors.
+ /// If one of the connections fails, the anchoring operation fails.
+ ///
+ /// The anchors this object wants to connect
+ /// The anchors this object wants to connect to
+ /// Whether to anchor scales too
+ /// Whether the anchoring succeeded or not
+ public bool AnchorToMultiple(IList myAnchors, IList targetAnchors, bool matchScales=true)
+ {
+ if (myAnchors.Count != targetAnchors.Count)
+ {
+ Debug.LogError("Error: Number of anchors must match.");
+ return false;
+ }
+ if (myAnchors.Count == 0)
+ {
+ Debug.LogError("Amount of anchors to connect must be at least 1.");
+ return false;
+ }
+
+ // Connect the first anchors. Then, test if other anchors match.
+ AnchorTo(myAnchors[0], targetAnchors[0], matchScales);
+ for (int i=1; i