From b6fee6fe2c74d8237b02389f0f2f2c2ecb94b6a0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tobias=20Sch=C3=B6ner?= <tobias.stonehead@gmail.com>
Date: Wed, 11 Jan 2023 18:01:17 +0100
Subject: [PATCH] feat: FactExplorer now displays lines between facts

Implemented UILine

Parent facts are connected to the main fact by a direct dashed line whereas child facts are connected by a jagged continuous line.
---
 .../UI/FactExplorer/FactExplorerUI.prefab     | 121 ++++++++++-
 .../UI/FactExplorer/FactExplorer_Line.prefab  |  93 +++++++++
 .../FactExplorer_Line.prefab.meta             |   7 +
 .../FactExplorer_Line_Dashed.prefab           | 141 +++++++++++++
 .../FactExplorer_Line_Dashed.prefab.meta      |   7 +
 .../Scripts/UI/FactExplorer/FactExplorer.cs   |  98 +++++++--
 Assets/Scripts/UI/FactExplorer/UILine.cs      | 190 ++++++++++++++++++
 Assets/Scripts/UI/FactExplorer/UILine.cs.meta |  11 +
 8 files changed, 643 insertions(+), 25 deletions(-)
 create mode 100644 Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line.prefab
 create mode 100644 Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line.prefab.meta
 create mode 100644 Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line_Dashed.prefab
 create mode 100644 Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line_Dashed.prefab.meta
 create mode 100644 Assets/Scripts/UI/FactExplorer/UILine.cs
 create mode 100644 Assets/Scripts/UI/FactExplorer/UILine.cs.meta

diff --git a/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorerUI.prefab b/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorerUI.prefab
index 37209044..f61c17ed 100644
--- a/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorerUI.prefab
+++ b/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorerUI.prefab
@@ -78,6 +78,103 @@ MonoBehaviour:
   m_EditorClassIdentifier: 
   m_HorizontalFit: 2
   m_VerticalFit: 2
+--- !u!1 &196909836272952088
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 294307632257003305}
+  - component: {fileID: 3259288062294702885}
+  - component: {fileID: 5601304647299485279}
+  - component: {fileID: 2468798738105667105}
+  m_Layer: 5
+  m_Name: Highlight
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &294307632257003305
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 196909836272952088}
+  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 2636669742664082113}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0.5, y: 0.5}
+  m_AnchorMax: {x: 0.5, y: 0.5}
+  m_AnchoredPosition: {x: 0, y: 0}
+  m_SizeDelta: {x: 110, y: 110}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &3259288062294702885
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 196909836272952088}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_IgnoreLayout: 1
+  m_MinWidth: -1
+  m_MinHeight: -1
+  m_PreferredWidth: -1
+  m_PreferredHeight: -1
+  m_FlexibleWidth: -1
+  m_FlexibleHeight: -1
+  m_LayoutPriority: 1
+--- !u!222 &5601304647299485279
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 196909836272952088}
+  m_CullTransparentMesh: 1
+--- !u!114 &2468798738105667105
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 196909836272952088}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 0.49803922, b: 0, a: 1}
+  m_RaycastTarget: 1
+  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+  m_Maskable: 1
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_Sprite: {fileID: 10911, guid: 0000000000000000f000000000000000, type: 0}
+  m_Type: 1
+  m_PreserveAspect: 0
+  m_FillCenter: 1
+  m_FillMethod: 4
+  m_FillAmount: 1
+  m_FillClockwise: 1
+  m_FillOrigin: 0
+  m_UseSpriteMesh: 0
+  m_PixelsPerUnitMultiplier: 1
 --- !u!1 &2146237966352740759
 GameObject:
   m_ObjectHideFlags: 0
@@ -87,6 +184,7 @@ GameObject:
   serializedVersion: 6
   m_Component:
   - component: {fileID: 2636669742664082113}
+  - component: {fileID: 8821645608645168199}
   m_Layer: 5
   m_Name: MainFact
   m_TagString: Untagged
@@ -105,7 +203,8 @@ RectTransform:
   m_LocalPosition: {x: 0, y: 0, z: 0}
   m_LocalScale: {x: 1, y: 1, z: 1}
   m_ConstrainProportionsScale: 0
-  m_Children: []
+  m_Children:
+  - {fileID: 294307632257003305}
   m_Father: {fileID: 3406631590813364790}
   m_RootOrder: 2
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
@@ -114,6 +213,14 @@ RectTransform:
   m_AnchoredPosition: {x: 0, y: 0}
   m_SizeDelta: {x: 100, y: 100}
   m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &8821645608645168199
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2146237966352740759}
+  m_CullTransparentMesh: 1
 --- !u!1 &5592893003942163674
 GameObject:
   m_ObjectHideFlags: 0
@@ -253,11 +360,11 @@ MonoBehaviour:
   mainFactUI: {fileID: 2636669742664082113}
   factChildrenUI: {fileID: 9153153668998730241}
   linesUI: {fileID: 8009565381998387591}
-  factExplorerLineParents_Prefab: {fileID: 602666608487228118, guid: 6cdb6cc4d5af8db409745afbc201068b,
+  factSpotPrefab: {fileID: 7124463502404826001, guid: 04d12e0a0a5aa884c8c7dff56f4963f6,
     type: 3}
-  factExplorerLineChildren_Prefab: {fileID: 4365600632079246398, guid: a9fcb9d279efe6849a46855110d5e794,
+  parentLine: {fileID: 602666608487228118, guid: 6cdb6cc4d5af8db409745afbc201068b,
     type: 3}
-  factSpotPrefab: {fileID: 7124463502404826001, guid: 04d12e0a0a5aa884c8c7dff56f4963f6,
+  childLine: {fileID: 4903779976374899580, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
     type: 3}
 --- !u!1 &7302347448387664938
 GameObject:
@@ -369,10 +476,10 @@ RectTransform:
   m_Father: {fileID: 3406631590813364790}
   m_RootOrder: 0
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
-  m_AnchorMin: {x: 0.5, y: 0.5}
-  m_AnchorMax: {x: 0.5, y: 0.5}
+  m_AnchorMin: {x: 0, y: 0}
+  m_AnchorMax: {x: 1, y: 1}
   m_AnchoredPosition: {x: 0, y: 0}
-  m_SizeDelta: {x: 100, y: 100}
+  m_SizeDelta: {x: 0, y: 0}
   m_Pivot: {x: 0.5, y: 0.5}
 --- !u!114 &835707716098452998
 MonoBehaviour:
diff --git a/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line.prefab b/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line.prefab
new file mode 100644
index 00000000..bb48d9e2
--- /dev/null
+++ b/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line.prefab
@@ -0,0 +1,93 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &4903779976374899580
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 911949952148793464}
+  - component: {fileID: -137166815004959870}
+  - component: {fileID: 2132054669896436896}
+  - component: {fileID: 4832178158615009880}
+  m_Layer: 0
+  m_Name: FactExplorer_Line
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &911949952148793464
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 4903779976374899580}
+  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 0}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0.5, y: 0.5}
+  m_AnchorMax: {x: 0.5, y: 0.5}
+  m_AnchoredPosition: {x: 0, y: 0}
+  m_SizeDelta: {x: 0, y: 0}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &-137166815004959870
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 4903779976374899580}
+  m_CullTransparentMesh: 1
+--- !u!114 &2132054669896436896
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 4903779976374899580}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_IgnoreLayout: 1
+  m_MinWidth: -1
+  m_MinHeight: -1
+  m_PreferredWidth: -1
+  m_PreferredHeight: -1
+  m_FlexibleWidth: -1
+  m_FlexibleHeight: -1
+  m_LayoutPriority: 1
+--- !u!114 &4832178158615009880
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 4903779976374899580}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 8ed745d97410d6740921398c899d9ec0, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 0.3137255, g: 0.3137255, b: 0.3137255, a: 1}
+  m_RaycastTarget: 0
+  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+  points: []
+  width: 4
+  roundCorners: 1
+  roundStart: 1
+  roundEnd: 1
+  dashed: 0
+  dashLength: 10
+  dashSpacing: 5
diff --git a/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line.prefab.meta b/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line.prefab.meta
new file mode 100644
index 00000000..a597efb2
--- /dev/null
+++ b/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 7f47b2a4ac81f444eb5e18b8efabc644
+PrefabImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line_Dashed.prefab b/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line_Dashed.prefab
new file mode 100644
index 00000000..092bb44b
--- /dev/null
+++ b/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line_Dashed.prefab
@@ -0,0 +1,141 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1001 &5499075457957573034
+PrefabInstance:
+  m_ObjectHideFlags: 0
+  serializedVersion: 2
+  m_Modification:
+    m_TransformParent: {fileID: 0}
+    m_Modifications:
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_Pivot.x
+      value: 0.5
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_Pivot.y
+      value: 0.5
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_RootOrder
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_AnchorMax.x
+      value: 0.5
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_AnchorMax.y
+      value: 0.5
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_AnchorMin.x
+      value: 0.5
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_AnchorMin.y
+      value: 0.5
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_SizeDelta.x
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_SizeDelta.y
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_LocalPosition.x
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_LocalPosition.y
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_LocalPosition.z
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_LocalRotation.w
+      value: 1
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_LocalRotation.x
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_LocalRotation.y
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_LocalRotation.z
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_AnchoredPosition.x
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_AnchoredPosition.y
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_LocalEulerAnglesHint.x
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_LocalEulerAnglesHint.y
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 911949952148793464, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_LocalEulerAnglesHint.z
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 4832178158615009880, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: dashed
+      value: 1
+      objectReference: {fileID: 0}
+    - target: {fileID: 4903779976374899580, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_Name
+      value: FactExplorer_Line_Dashed
+      objectReference: {fileID: 0}
+    - target: {fileID: 8455242915356568467, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_Materials.Array.size
+      value: 1
+      objectReference: {fileID: 0}
+    - target: {fileID: 8455242915356568467, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_Positions.Array.size
+      value: 2
+      objectReference: {fileID: 0}
+    - target: {fileID: 8455242915356568467, guid: 7f47b2a4ac81f444eb5e18b8efabc644,
+        type: 3}
+      propertyPath: m_Materials.Array.data[0]
+      value: 
+      objectReference: {fileID: 2100000, guid: 3be144593ef3bac42812ba90847c1cff, type: 2}
+    m_RemovedComponents: []
+  m_SourcePrefab: {fileID: 100100000, guid: 7f47b2a4ac81f444eb5e18b8efabc644, type: 3}
diff --git a/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line_Dashed.prefab.meta b/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line_Dashed.prefab.meta
new file mode 100644
index 00000000..3ef635f8
--- /dev/null
+++ b/Assets/Resources/Prefabs/UI/FactExplorer/FactExplorer_Line_Dashed.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 6cdb6cc4d5af8db409745afbc201068b
+PrefabImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/UI/FactExplorer/FactExplorer.cs b/Assets/Scripts/UI/FactExplorer/FactExplorer.cs
index a1583f8b..199362a7 100644
--- a/Assets/Scripts/UI/FactExplorer/FactExplorer.cs
+++ b/Assets/Scripts/UI/FactExplorer/FactExplorer.cs
@@ -8,6 +8,7 @@
 
 public class FactExplorer : MonoBehaviour
 {
+    #region InspectorVariables
     [Header("PrefabComponents")]
     [SerializeField] private Transform factParentsUI;
     [SerializeField] private Transform mainFactUI;
@@ -16,13 +17,17 @@ public class FactExplorer : MonoBehaviour
 
     [Header("Prefabs")]
     [SerializeField] private GameObject factSpotPrefab;
+    [SerializeField] private GameObject parentLine;
+    [SerializeField] private GameObject childLine;
+    #endregion InspectorVariables
 
-
+    #region Variables
     private Fact mainFact;
     private List<Fact> parentFacts;
     private List<Fact> childFacts;
+    #endregion Variables
 
-
+    #region UnityMethods
     private void Update()
     {
         DestroyIfClickedOutside();
@@ -30,7 +35,6 @@ private void Update()
 
     public void Initialize(Fact fact)
     {
-
         mainFact = fact;
         parentFacts = GetParentFacts();
         childFacts = GetChildFacts();
@@ -40,6 +44,7 @@ public void Initialize(Fact fact)
 
         UpdateFactExplorerUI();
     }
+    #endregion UnityMethods
 
     #region Implementation
     private List<Fact> GetParentFacts()
@@ -72,18 +77,88 @@ private void UpdateFactExplorerUI()
         SpawnUIFacts(factParentsUI, parentFacts);
         SpawnUIFacts(mainFactUI, new List<Fact>() { mainFact });
         SpawnUIFacts(factChildrenUI, childFacts);
+
+        // Force rebuild of FactExplorer layout, since the positions of the factObjects will be wrong otherwise
+        LayoutRebuilder.ForceRebuildLayoutImmediate(transform.GetComponent<RectTransform>());
+
+        SpawnParentLines(factParentsUI.gameObject, mainFactUI);
+        SpawnChildLines(factChildrenUI.gameObject, mainFactUI);
     }
 
     private void DestroyIfClickedOutside()
     {
-        if ((Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1)) &&
-            !RectTransformUtility.RectangleContainsScreenPoint(transform.GetComponent<RectTransform>(), Input.mousePosition, null))
+        // Destroy on tab press or left click outside of FactExplorer
+        if (Input.GetKeyDown(KeyCode.Tab) 
+            || (Input.GetMouseButtonDown(0) && !RectTransformUtility.RectangleContainsScreenPoint(transform.GetComponent<RectTransform>(), Input.mousePosition, null)))
         {
             Destroy(gameObject);
         }
     }
     #endregion Implementation
 
+    #region Spawner
+    private void SpawnUIFacts(Transform uiParent, List<Fact> toSpawn)
+    {
+        // if uiParent has no children: deactivate it
+        if (toSpawn.Count == 0)
+            uiParent.gameObject.SetActive(false);
+
+        foreach (Fact f in toSpawn)
+        {
+            var spot = Instantiate(factSpotPrefab, uiParent);
+
+            // TODO: this link to DisplayFacts is not ideal: maybe refactor to SciptableObject or such
+            var display = f.instantiateDisplay(DisplayFacts.prefabDictionary[f.GetType()], spot.transform);
+            display.transform.localPosition = Vector3.zero;
+        }
+    }
+
+    private void SpawnParentLines(GameObject parent, Transform mainFactUI)
+    {
+        var mainTransform = mainFactUI.GetComponent<RectTransform>();
+        var factWidth = mainTransform.rect.width;
+        // transform.positions are weird due to LayoutGroups => manually calculate offset
+        float xOffset = -factParentsUI.GetComponent<RectTransform>().rect.width / 2 +  factWidth / 2;
+        float yOffset = transform.GetComponent<VerticalLayoutGroup>().spacing;
+        parent.ForAllChildren(par =>
+        {
+            // position at the bottom center of par rect
+            var position = par.transform.TransformPoint(new Vector2(0, par.GetComponent<RectTransform>().rect.yMin));
+            var line = Instantiate(parentLine, position, Quaternion.identity, par.transform);
+
+            var uiLine = line.GetComponent<UILine>();
+            uiLine.points = new List<Vector2>() { Vector2.zero, new Vector2(-xOffset, -yOffset) };
+
+            xOffset += factWidth + factParentsUI.GetComponent<HorizontalLayoutGroup>().spacing;
+        });
+    }
+
+    private void SpawnChildLines(GameObject parent, Transform mainFactUI)
+    {
+        var mainTransform = mainFactUI.GetComponent<RectTransform>();
+        var factWidth = mainTransform.rect.width;
+        // transform.positions are weird due to LayoutGroups => manually calculate offset
+        float xOffset = -factChildrenUI.GetComponent<RectTransform>().rect.width / 2 + factWidth / 2;
+        float yOffset = -transform.GetComponent<VerticalLayoutGroup>().spacing;
+        parent.ForAllChildren(par =>
+        {
+            // position at the top center of par rect
+            var position = par.transform.TransformPoint(new Vector2(0, par.GetComponent<RectTransform>().rect.yMax));
+            var line = Instantiate(childLine, position, Quaternion.identity, par.transform);
+
+            var uiLine = line.GetComponent<UILine>();
+            uiLine.points = new List<Vector2>() {
+                Vector2.zero, 
+                new Vector2(0, -yOffset/2),
+                new Vector2(-xOffset, -yOffset/2),
+                new Vector2(-xOffset, -yOffset)
+            };
+
+            xOffset += factWidth + factChildrenUI.GetComponent<HorizontalLayoutGroup>().spacing;
+        });
+    }
+    #endregion Spawner
+
     #region Helper
     /// <summary>
     /// Check all fields of a Fact for references to other facts and return their ids
@@ -96,18 +171,5 @@ private static IEnumerable<string> GetReferencedPids(Fact f)
             .Where(field => field.FieldType == typeof(string) && field.Name.ToLower().StartsWith("pid"))
         ).Select(fi => (string)fi.GetValue(f));
     }
-
-    private void SpawnUIFacts(Transform uiParent, List<Fact> toSpawn)
-    {
-        uiParent.gameObject.DestroyAllChildren();
-        foreach (Fact f in toSpawn)
-        {
-            var spot = Instantiate(factSpotPrefab, uiParent);
-
-            // TODO: this link to DisplayFacts is not ideal: maybe refactor to SciptableObject or such
-            var display = f.instantiateDisplay(DisplayFacts.prefabDictionary[f.GetType()], spot.transform);
-            display.transform.localPosition = Vector3.zero;
-        }
-    }
     #endregion Helper
 }
diff --git a/Assets/Scripts/UI/FactExplorer/UILine.cs b/Assets/Scripts/UI/FactExplorer/UILine.cs
new file mode 100644
index 00000000..b8c56f42
--- /dev/null
+++ b/Assets/Scripts/UI/FactExplorer/UILine.cs
@@ -0,0 +1,190 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.UI;
+using System.Linq;
+
+public class UILine : Graphic
+{
+    #region InspectorVariables
+    [Header("General")]
+    public List<Vector2> points = new();
+    public float width = 5f;
+
+    [Header("Rounding")]
+    public bool roundCorners = true;
+    public bool roundStart = false;
+    public bool roundEnd = false;
+
+    [Header("Dashed")]
+    public bool dashed = false;
+    public int dashLength = 10;
+    public int dashSpacing = 5;
+    #endregion InspectorVariables
+
+    #region UnityMethods
+    protected override void OnPopulateMesh(VertexHelper vh)
+    {
+        // clear old vertecies and triangles
+        vh.Clear();
+
+        // a line with less than 2 points does not make any sense => return
+        if (points.Count < 2)
+            return;
+
+        if (dashed)
+            GenerateLinesDashed(vh);
+        else
+            GenerateLinesStandard(vh);
+
+        if (roundStart)
+            CreateCircle(vh, points[0], width);
+
+        if (roundEnd)
+            CreateCircle(vh, points[^1], width);
+    }
+    #endregion UnityMethods
+
+    #region Implementation
+
+    #region GenerateLines
+    private void GenerateLinesStandard(VertexHelper vh)
+    {
+        points.Zip(points.Skip(1), (a, b) => Tuple.Create(a, b))
+            .ToList()
+            .ForEach(tup => CreateSegment(vh, tup.Item1, tup.Item2));
+
+        if (roundCorners)
+        {
+            // take all points except for start and end and apply rounding
+            points.Take(points.Count - 1).Skip(1).ToList()
+                .ForEach(p => CreateCircle(vh, p, width));
+        }
+    }
+
+    private void GenerateLinesDashed(VertexHelper vh)
+    {
+        float restLen = dashLength;
+        bool isDash = true;
+        var lines = points.Zip(points.Skip(1), (a, b) => Tuple.Create(a, b)).ToList();
+        for (int i = 0; i < lines.Count; i++)
+        {
+            var tup = lines[i];
+            Vector2 start = tup.Item1;
+            Vector2 end = tup.Item2;
+            Vector2 dir = (end - start).normalized;
+
+            // main spacing algorithm
+            Vector2 current = start;
+            while (Vector2.Distance(current, end) >= restLen)
+            {
+                Vector2 segmentEnd = current + restLen * dir;
+                if (isDash)
+                    CreateSegment(vh, current, segmentEnd);
+                current = segmentEnd;
+                isDash = !isDash;
+                restLen = isDash ? dashLength : dashSpacing;
+            }
+
+            // is there a dash wrapping around a corner?
+            bool dashOverCorner = false;
+
+            
+            float distLeft = Vector2.Distance(current, end);
+            if (!isDash)
+                restLen -= distLeft;
+            // dont fill remaining distance with dash, if it would be short (shorter than width/2)
+            else if (isDash && distLeft > width/2)
+            {
+                CreateSegment(vh, current, end);
+                restLen -= distLeft;
+                dashOverCorner = true;
+            }
+
+            // discard rest of dash if it is too short (shorter than width/2)
+            if (isDash && restLen < width/2)
+            {
+                isDash = false;
+                restLen = dashSpacing;
+                dashOverCorner = false; // dash does not wrap around corner as dashSpacing will follow
+            }
+
+            // only create round corners if roundCorners are enabled and there is a dash wrapping around the corner and not last corner (aka end)
+            if (roundCorners && dashOverCorner && i != lines.Count-1)
+                CreateCircle(vh, end, width);
+        }
+    }
+    #endregion GenerateLines
+
+    #region CreateLine
+    private void CreateSegment(VertexHelper vh, Vector2 start, Vector2 end)
+    {
+        //start += (Vector2)transform.position;
+        //end += (Vector2)transform.position;
+
+        UIVertex vertex = UIVertex.simpleVert;
+        vertex.color = color;
+
+        Vector2 dir = end - start;
+        Vector2 perp = Vector2.Perpendicular(dir).normalized;
+        Vector2 off = perp * width / 2;
+
+        vertex.position = new Vector3(start.x + off.x, start.y + off.y);
+        vh.AddVert(vertex);
+        vertex.position = new Vector3(end.x + off.x, end.y + off.y);
+        vh.AddVert(vertex);
+        vertex.position = new Vector3(end.x - off.x, end.y - off.y);
+        vh.AddVert(vertex);
+        vertex.position = new Vector3(start.x - off.x, start.y - off.y);
+        vh.AddVert(vertex);
+
+        int offset = vh.currentVertCount - 4;
+        vh.AddTriangle(0 + offset, 1 + offset, 2 + offset);
+        vh.AddTriangle(2 + offset, 3 + offset, 0 + offset);
+    }
+    #endregion CreateLine
+
+    #region Rounding
+    private void CreateCircle(VertexHelper vh, Vector2 center, float diameter, int sideCount = 20)
+    {
+        //center += (Vector2)transform.position;
+
+        UIVertex vertex = UIVertex.simpleVert;
+        vertex.color = color;
+
+        Vector3[] vertices = GetCirclePoints(diameter/2, sideCount, center).Union(new Vector3[] { center }).ToArray();
+        vertices.ToList().ForEach(vert => {
+                vertex.position = vert;
+                vh.AddVert(vertex);
+            }
+        );
+
+        int startVert = vh.currentVertCount - vertices.Length;
+        for (int i = 0; i < vertices.Length - 1; i++)
+        {
+            vh.AddTriangle(
+                vh.currentVertCount - 1, // center
+                startVert + i,
+                startVert + ((i + 1) % (vertices.Length - 1))
+            ); 
+        }
+        return;
+    }
+
+    protected static Vector3[] GetCirclePoints(float circleRadius, int pointCount, Vector3 offset)
+    {
+        Vector3[] circle = new Vector3[pointCount];
+        float slice = (2f * Mathf.PI) / pointCount;
+        for (int i = 0; i < pointCount; i++)
+        {
+            float angle = i * slice;
+            circle[i] = new Vector3(circleRadius * Mathf.Sin(angle), circleRadius * Mathf.Cos(angle)) + offset;
+        }
+        return circle;
+    }
+
+    #endregion Rounding
+
+    #endregion Implementation
+}
diff --git a/Assets/Scripts/UI/FactExplorer/UILine.cs.meta b/Assets/Scripts/UI/FactExplorer/UILine.cs.meta
new file mode 100644
index 00000000..1633aef8
--- /dev/null
+++ b/Assets/Scripts/UI/FactExplorer/UILine.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8ed745d97410d6740921398c899d9ec0
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
-- 
GitLab