Merge branch 'BTS-135/leoqu' into 'main'

Added ik initial mode to UI

See merge request capstone-group2/ui!4
This commit is contained in:
Leo Qu
2026-02-20 14:32:02 -05:00
10 changed files with 2195 additions and 11 deletions

View File

@@ -6,9 +6,9 @@ PluginImporter:
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 1
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
isExplicitlyReferenced: 1
validateReferences: 1
platformData:
- first:

8
Assets/IK.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 748b1f7addcb948b49543a462fedf0fe
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,409 @@
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
public class InverseKinematicsController : MonoBehaviour
{
[Header("IK Settings")]
public int maxIterations = 10;
public float tolerance = 0.1f;
public bool constrainAngles = true;
[Header("Per-Joint Rotation Deadzone")]
[Tooltip("For ServoStraightModule joints only: if CCD computes a rotation smaller than this many degrees for a joint, that joint will not be updated this step.")]
[Min(0f)]
public float minRotationStepDegreesForDcAndStraight = 10f;
[Header("Drag Sensitivity")]
[Tooltip("How much of the user's mouse drag movement is interpreted (1.0 = 100%, 0.2 = 20%)")]
[Range(0.01f, 2.0f)]
public float dragSensitivity = 0.7f;
[Header("Movement Smoothing")]
[Range(0.01f, 1f)]
public float movementSpeed = 0.3f;
public bool useSmoothing = true;
[Header("Debug")]
public bool drawDebugLines = true;
public Color debugLineColor = Color.yellow;
private List<IKJoint> kinematicChain = new List<IKJoint>();
private Vector3 targetPosition;
private Vector3 smoothedTargetPosition;
private Transform endEffector;
private Transform anchorPoint;
private Vector3 anchorPosition;
private bool isSolving = false;
public bool BuildKinematicChain(Transform targetModule)
{
kinematicChain.Clear();
endEffector = targetModule;
Transform root = FindTopologyRoot(targetModule);
if (root == null || root.name != "GeneratedTopology")
{
return false;
}
BuildChainFromTargetToRoot(targetModule, root);
kinematicChain.Reverse();
if (kinematicChain.Count > 0)
{
Transform firstBase = kinematicChain[0].baseTransform != null ? kinematicChain[0].baseTransform : kinematicChain[0].pivotTransform;
Transform foundAnchor = FindAnchorPoint(firstBase);
if (foundAnchor != null)
{
anchorPoint = foundAnchor;
anchorPosition = anchorPoint.position;
}
else
{
anchorPoint = firstBase;
anchorPosition = anchorPoint.position;
}
}
else
{
Transform foundAnchor = FindAnchorPoint(endEffector);
if (foundAnchor != null)
{
anchorPoint = foundAnchor;
anchorPosition = anchorPoint.position;
}
}
ResetSmoothing();
return kinematicChain.Count > 0;
}
private void BuildChainFromTargetToRoot(Transform targetModule, Transform root)
{
Transform current = targetModule;
bool skippedSelectedModuleJoint = false;
while (current != null && current != root)
{
ServoMotorModule servo = current.GetComponentInParent<ServoMotorModule>();
if (servo != null && servo.armPivot != null)
{
if (!kinematicChain.Any(j => j.servo == servo))
{
if (!skippedSelectedModuleJoint)
{
skippedSelectedModuleJoint = true;
current = current.parent;
continue;
}
IKJoint joint = new IKJoint
{
servo = servo,
pivotTransform = servo.armPivot,
baseTransform = servo.transform,
minAngle = 0f,
maxAngle = 180f,
currentAngle = servo.currentAngle
};
// Critical: store axis in pivot-local space; compute world axis each iteration.
if (servo is ServoBendModule)
{
joint.localRotationAxis = Vector3.forward;
}
else if (servo is ServoStraightModule)
{
joint.localRotationAxis = Vector3.right;
}
else
{
joint.localRotationAxis = Vector3.up;
}
kinematicChain.Add(joint);
}
}
current = current.parent;
}
}
public bool SolveIK(Vector3 targetPos)
{
if (kinematicChain.Count == 0)
{
return false;
}
if (useSmoothing)
{
if (smoothedTargetPosition == Vector3.zero)
{
smoothedTargetPosition = endEffector != null ? endEffector.position : targetPos;
}
smoothedTargetPosition = Vector3.Lerp(smoothedTargetPosition, targetPos, movementSpeed);
targetPosition = smoothedTargetPosition;
}
else
{
targetPosition = targetPos;
}
isSolving = true;
bool success = SolveCCD();
isSolving = false;
return success;
}
private bool SolveCCD()
{
for (int iteration = 0; iteration < maxIterations; iteration++)
{
for (int i = kinematicChain.Count - 1; i >= 0; i--)
{
IKJoint joint = kinematicChain[i];
Vector3 endEffectorPos = endEffector.position;
Vector3 jointPos = joint.pivotTransform.position;
if (i == 0 && anchorPoint != null)
{
jointPos = anchorPosition;
}
Vector3 toEndEffector = endEffectorPos - jointPos;
Vector3 toTarget = targetPosition - jointPos;
if (toEndEffector.magnitude < 0.001f || toTarget.magnitude < 0.001f)
continue;
Vector3 worldAxis = joint.pivotTransform.TransformDirection(joint.localRotationAxis).normalized;
float angle = Vector3.SignedAngle(toEndEffector, toTarget, worldAxis);
float desiredAngle = joint.currentAngle + angle;
if (constrainAngles)
{
desiredAngle = Mathf.Clamp(desiredAngle, joint.minAngle, joint.maxAngle);
}
float appliedDelta = desiredAngle - joint.currentAngle;
bool isStraightServo = joint.servo is ServoStraightModule;
if (isStraightServo && Mathf.Abs(appliedDelta) < minRotationStepDegreesForDcAndStraight)
{
continue;
}
joint.currentAngle = desiredAngle;
joint.servo.SetAngle(desiredAngle);
endEffectorPos = endEffector.position;
if (Vector3.Distance(endEffectorPos, targetPosition) < tolerance)
{
return true;
}
}
}
float finalDistance = Vector3.Distance(endEffector.position, targetPosition);
return finalDistance < tolerance * 10f;
}
public void ResetSmoothing()
{
smoothedTargetPosition = Vector3.zero;
}
public Transform GetAnchorPoint()
{
return anchorPoint;
}
public bool IsAnchorPoint(Transform moduleTransform)
{
return anchorPoint != null && anchorPoint == moduleTransform;
}
private Transform FindTopologyRoot(Transform module)
{
Transform current = module;
while (current != null)
{
if (current.name == "GeneratedTopology")
{
return current;
}
current = current.parent;
}
return null;
}
private Transform FindAnchorPoint(Transform servoTransform)
{
Transform topologyRoot = FindTopologyRoot(servoTransform);
if (topologyRoot == null)
{
return servoTransform;
}
Transform batteryOrHub = FindBatteryOrHub(topologyRoot);
if (batteryOrHub != null)
{
return batteryOrHub;
}
Transform endModule = FindEndModule(topologyRoot);
if (endModule != null)
{
return endModule;
}
return servoTransform;
}
private Transform FindBatteryOrHub(Transform topologyRoot)
{
foreach (Transform child in topologyRoot)
{
ModuleBase module = child.GetComponent<ModuleBase>();
if (module != null)
{
BatteryModule battery = child.GetComponent<BatteryModule>();
HubModule hub = child.GetComponent<HubModule>();
if (battery != null || hub != null)
{
return child;
}
}
}
return null;
}
private Transform FindEndModule(Transform topologyRoot)
{
List<Transform> allModules = new List<Transform>();
foreach (Transform child in topologyRoot)
{
ModuleBase module = child.GetComponent<ModuleBase>();
if (module != null)
{
allModules.Add(child);
}
}
foreach (Transform module in allModules)
{
int connectionCount = CountConnections(module, allModules);
if (connectionCount == 1)
{
return module;
}
}
return null;
}
private int CountConnections(Transform module, List<Transform> allModules)
{
int count = 0;
Transform currentParent = module.parent;
if (currentParent != null && currentParent.name != "GeneratedTopology")
{
ModuleBase parentModule = currentParent.GetComponent<ModuleBase>();
if (parentModule != null)
{
count++;
}
else
{
Transform checkParent = currentParent.parent;
while (checkParent != null && checkParent.name != "GeneratedTopology")
{
ModuleBase checkModule = checkParent.GetComponent<ModuleBase>();
if (checkModule != null)
{
count++;
break;
}
checkParent = checkParent.parent;
}
}
}
foreach (Transform otherModule in allModules)
{
if (otherModule == module) continue;
Transform checkParent = otherModule.parent;
while (checkParent != null && checkParent.name != "GeneratedTopology")
{
if (checkParent == module)
{
count++;
break;
}
checkParent = checkParent.parent;
}
}
return count;
}
void OnDrawGizmos()
{
if (!drawDebugLines || kinematicChain.Count == 0)
return;
Gizmos.color = debugLineColor;
Vector3 prevPos;
if (anchorPoint != null)
{
prevPos = anchorPosition;
Gizmos.color = Color.green;
Gizmos.DrawSphere(prevPos, 0.15f);
Gizmos.color = debugLineColor;
}
else
{
prevPos = kinematicChain[0].pivotTransform.position;
}
for (int i = 0; i < kinematicChain.Count; i++)
{
Vector3 currentPos = kinematicChain[i].pivotTransform.position;
Gizmos.DrawLine(prevPos, currentPos);
Gizmos.DrawSphere(currentPos, 0.1f);
prevPos = currentPos;
}
if (endEffector != null)
{
Gizmos.DrawLine(prevPos, endEffector.position);
Gizmos.DrawSphere(endEffector.position, 0.15f);
}
Gizmos.color = Color.red;
Gizmos.DrawSphere(targetPosition, 0.2f);
Gizmos.DrawLine(endEffector != null ? endEffector.position : prevPos, targetPosition);
}
private class IKJoint
{
public ServoMotorModule servo;
public Transform pivotTransform;
public Transform baseTransform;
public Vector3 localRotationAxis;
public float minAngle;
public float maxAngle;
public float currentAngle;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aed8b5469e22847cbab133f3f20750b2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,585 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using System.Collections.Generic;
public class ModulePositionController : MonoBehaviour
{
[Header("References")]
public Camera mainCamera;
public InverseKinematicsController ikController;
public LayerMask moduleLayer = -1; // All layers by default
[Header("Position Control Settings")]
public float positionStepSize = 0.1f; // How much to move per button click
public float positionChangeThreshold = 0.01f; // Minimum position change to trigger IK solve
[Header("Visual Feedback")]
public Color selectedColor = new Color(0f, 0.8f, 1f, 1f); // Cyan color
[Header("Selected Module Transparency (IK View)")]
[Tooltip("If enabled, the currently selected module becomes transparent in IK view.")]
public bool makeSelectedModuleTransparent = true;
[Range(0.05f, 1f)]
public float selectedModuleAlpha = 0.25f;
[Header("UI References")]
public Button xPlusButton;
public Button xMinusButton;
public Button yPlusButton;
public Button yMinusButton;
public Button zPlusButton;
public Button zMinusButton;
[Header("Gizmo")]
public PositionControlGizmo gizmo;
[HideInInspector] public ModuleBase selectedModule;
private Vector3 targetPosition;
private Vector3 lastSuccessfulPosition;
private bool[] axisLimitReached = new bool[3];
private int[] axisLimitDirection = new int[3];
private bool gizmoWasClicked = false;
// Critical: modules are parented; apply transparency only to renderers owned by the selected module.
private readonly Dictionary<Renderer, Material[]> _originalSharedMaterialsByRenderer = new();
private readonly List<Renderer> _transparentRenderers = new();
void Start()
{
if (mainCamera == null)
{
mainCamera = Camera.main;
if (mainCamera == null)
{
mainCamera = FindObjectOfType<Camera>();
}
}
if (ikController == null)
{
ikController = GetComponentInParent<InverseKinematicsController>();
if (ikController == null)
{
ikController = FindObjectOfType<InverseKinematicsController>();
}
}
if (gizmo == null)
{
gizmo = GetComponentInChildren<PositionControlGizmo>();
if (gizmo == null)
{
gizmo = FindObjectOfType<PositionControlGizmo>();
}
if (gizmo != null && gizmo.positionController == null)
{
gizmo.positionController = this;
}
}
SetupUIButtons();
}
void SetupUIButtons()
{
if (xPlusButton != null) xPlusButton.onClick.RemoveAllListeners();
if (xMinusButton != null) xMinusButton.onClick.RemoveAllListeners();
if (yPlusButton != null) yPlusButton.onClick.RemoveAllListeners();
if (yMinusButton != null) yMinusButton.onClick.RemoveAllListeners();
if (zPlusButton != null) zPlusButton.onClick.RemoveAllListeners();
if (zMinusButton != null) zMinusButton.onClick.RemoveAllListeners();
if (xPlusButton != null)
{
xPlusButton.onClick.AddListener(() => AdjustPositionAxis(0, 1));
}
if (xMinusButton != null)
{
xMinusButton.onClick.AddListener(() => AdjustPositionAxis(0, -1));
}
if (yPlusButton != null)
{
yPlusButton.onClick.AddListener(() => AdjustPositionAxis(1, 1));
}
if (yMinusButton != null)
{
yMinusButton.onClick.AddListener(() => AdjustPositionAxis(1, -1));
}
if (zPlusButton != null)
{
zPlusButton.onClick.AddListener(() => AdjustPositionAxis(2, 1));
}
if (zMinusButton != null)
{
zMinusButton.onClick.AddListener(() => AdjustPositionAxis(2, -1));
}
}
public void AdjustPositionAxis(int axis, int sign)
{
if (selectedModule == null)
{
return;
}
if (ikController == null)
{
return;
}
Vector3 currentActualPosition = selectedModule.transform.position;
// Critical: preserve user intent on the other axes when adjusting only one axis.
float targetOtherAxisX = targetPosition.x;
float targetOtherAxisY = targetPosition.y;
float targetOtherAxisZ = targetPosition.z;
Vector3 positionBeforeIK = currentActualPosition;
Vector3 newTargetPosition = targetPosition;
switch (axis)
{
case 0: // X axis
newTargetPosition.x += sign * positionStepSize;
break;
case 1: // Y axis
newTargetPosition.y += sign * positionStepSize;
break;
case 2: // Z axis
newTargetPosition.z += sign * positionStepSize;
break;
}
float currentAxisValue = 0f;
float newAxisValue = 0f;
switch (axis)
{
case 0:
currentAxisValue = positionBeforeIK.x;
newAxisValue = newTargetPosition.x;
break;
case 1:
currentAxisValue = positionBeforeIK.y;
newAxisValue = newTargetPosition.y;
break;
case 2:
currentAxisValue = positionBeforeIK.z;
newAxisValue = newTargetPosition.z;
break;
}
targetPosition = newTargetPosition;
if (ikController != null)
{
ikController.ResetSmoothing();
}
bool success = false;
if (ikController != null)
{
success = ikController.SolveIK(targetPosition);
}
Vector3 positionAfterIK = selectedModule.transform.position;
float axisMovement = 0f;
float axisValueBefore = 0f;
float axisValueAfter = 0f;
switch (axis)
{
case 0: // X axis
axisValueBefore = positionBeforeIK.x;
axisValueAfter = positionAfterIK.x;
axisMovement = Mathf.Abs(axisValueAfter - axisValueBefore);
break;
case 1: // Y axis
axisValueBefore = positionBeforeIK.y;
axisValueAfter = positionAfterIK.y;
axisMovement = Mathf.Abs(axisValueAfter - axisValueBefore);
break;
case 2: // Z axis
axisValueBefore = positionBeforeIK.z;
axisValueAfter = positionAfterIK.z;
axisMovement = Mathf.Abs(axisValueAfter - axisValueBefore);
break;
}
bool movementInCorrectDirection = false;
switch (axis)
{
case 0: // X axis
movementInCorrectDirection = (sign > 0 && axisValueAfter > axisValueBefore) || (sign < 0 && axisValueAfter < axisValueBefore);
break;
case 1: // Y axis
movementInCorrectDirection = (sign > 0 && axisValueAfter > axisValueBefore) || (sign < 0 && axisValueAfter < axisValueBefore);
break;
case 2: // Z axis
movementInCorrectDirection = (sign > 0 && axisValueAfter > axisValueBefore) || (sign < 0 && axisValueAfter < axisValueBefore);
break;
}
// Critical: prevent target drift when constraints move opposite the user's intent.
bool shouldUpdateTarget = (success || axisMovement >= positionChangeThreshold) && movementInCorrectDirection;
Vector3 updatedTargetPosition = targetPosition;
if (shouldUpdateTarget)
{
updatedTargetPosition = targetPosition;
switch (axis)
{
case 0:
updatedTargetPosition.x = positionAfterIK.x;
updatedTargetPosition.y = targetOtherAxisY;
updatedTargetPosition.z = targetOtherAxisZ;
break;
case 1:
updatedTargetPosition.x = targetOtherAxisX;
updatedTargetPosition.y = positionAfterIK.y;
updatedTargetPosition.z = targetOtherAxisZ;
break;
case 2:
updatedTargetPosition.x = targetOtherAxisX;
updatedTargetPosition.y = targetOtherAxisY;
updatedTargetPosition.z = positionAfterIK.z;
break;
}
targetPosition = updatedTargetPosition;
}
else
{
switch (axis)
{
case 0:
targetPosition.x = targetOtherAxisX;
break;
case 1:
targetPosition.y = targetOtherAxisY;
break;
case 2:
targetPosition.z = targetOtherAxisZ;
break;
}
updatedTargetPosition = targetPosition;
if (axisMovement >= positionChangeThreshold && !movementInCorrectDirection)
{
if (ikController != null)
{
ikController.ResetSmoothing();
}
}
}
lastSuccessfulPosition = positionAfterIK;
axisLimitReached[axis] = false;
axisLimitDirection[axis] = 0;
}
void LateUpdate()
{
HandleSelection();
}
void HandleSelection()
{
if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
return;
if (Input.GetMouseButtonDown(0))
{
gizmoWasClicked = false;
if (gizmo != null && gizmo.enabled && gizmo.WasGizmoClicked())
{
gizmoWasClicked = true;
return; // Don't process selection/deselection when gizmo arrow is clicked
}
if (gizmoWasClicked)
{
return;
}
if (mainCamera == null)
{
mainCamera = Camera.main;
if (mainCamera == null)
{
mainCamera = FindObjectOfType<Camera>();
}
if (mainCamera == null)
{
return;
}
}
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, Mathf.Infinity, moduleLayer))
{
ModuleBase hitModule = hit.collider.GetComponentInParent<ModuleBase>();
if (hitModule != null && IsPartOfGeneratedTopology(hitModule.transform))
{
SelectModule(hitModule);
}
else
{
DeselectModule();
}
}
else
{
if (!gizmoWasClicked)
{
DeselectModule();
}
}
}
}
public void OnGizmoClicked()
{
gizmoWasClicked = true;
}
void SelectModule(ModuleBase module)
{
if (selectedModule != null && selectedModule != module)
{
DeselectModule();
}
ServoMotorModule.selectedModule = null;
ObjectSelector.selectedObject = null;
selectedModule = module;
targetPosition = selectedModule.transform.position;
lastSuccessfulPosition = selectedModule.transform.position;
axisLimitReached[0] = false;
axisLimitReached[1] = false;
axisLimitReached[2] = false;
axisLimitDirection[0] = 0;
axisLimitDirection[1] = 0;
axisLimitDirection[2] = 0;
ServoMotorModule servoModule = module.GetComponent<ServoMotorModule>();
if (servoModule != null)
{
ServoMotorModule.selectedModule = servoModule;
}
else
{
ObjectSelector.selectedObject = module.gameObject;
}
if (gizmo != null)
{
gizmo.enabled = true;
gizmo.RefreshGizmoVisibility();
}
if (ikController != null)
{
bool chainBuilt = ikController.BuildKinematicChain(selectedModule.transform);
if (chainBuilt)
{
if (ikController.IsAnchorPoint(selectedModule.transform))
{
DeselectModule();
return;
}
}
ikController.ResetSmoothing();
}
if (makeSelectedModuleTransparent)
{
ApplyTransparencyToSelectedModule();
}
}
public void DeselectModule()
{
RestoreTransparency();
if (selectedModule != null)
{
ServoMotorModule.selectedModule = null;
ObjectSelector.selectedObject = null;
}
selectedModule = null;
if (gizmo != null)
{
gizmo.enabled = false;
gizmo.RefreshGizmoVisibility();
}
}
private void ApplyTransparencyToSelectedModule()
{
RestoreTransparency();
if (selectedModule == null) return;
// Critical: don't descend into other ModuleBase subtrees (child modules).
foreach (var r in GetRenderersOwnedByModule(selectedModule))
{
if (!_originalSharedMaterialsByRenderer.ContainsKey(r))
{
_originalSharedMaterialsByRenderer[r] = r.sharedMaterials;
}
Material[] mats = r.materials;
for (int i = 0; i < mats.Length; i++)
{
var m = mats[i];
if (m == null) continue;
ConfigureMaterialForTransparency(m, selectedModuleAlpha);
}
_transparentRenderers.Add(r);
}
}
private static IEnumerable<Renderer> GetRenderersOwnedByModule(ModuleBase rootModule)
{
if (rootModule == null) yield break;
Transform root = rootModule.transform;
var stack = new Stack<Transform>();
stack.Push(root);
while (stack.Count > 0)
{
Transform t = stack.Pop();
if (t == null) continue;
if (t != root)
{
ModuleBase other = t.GetComponent<ModuleBase>();
if (other != null && other != rootModule)
continue;
}
foreach (var r in t.GetComponents<Renderer>())
{
if (r != null)
yield return r;
}
for (int i = 0; i < t.childCount; i++)
{
stack.Push(t.GetChild(i));
}
}
}
private static void ConfigureMaterialForTransparency(Material m, float alpha)
{
alpha = Mathf.Clamp01(alpha);
if (m.HasProperty("_Surface"))
{
m.SetFloat("_Surface", 1f);
if (m.HasProperty("_Blend")) m.SetFloat("_Blend", 0f);
if (m.HasProperty("_AlphaClip")) m.SetFloat("_AlphaClip", 0f);
m.EnableKeyword("_SURFACE_TYPE_TRANSPARENT");
m.DisableKeyword("_SURFACE_TYPE_OPAQUE");
m.SetOverrideTag("RenderType", "Transparent");
if (m.HasProperty("_SrcBlend")) m.SetFloat("_SrcBlend", (float)UnityEngine.Rendering.BlendMode.SrcAlpha);
if (m.HasProperty("_DstBlend")) m.SetFloat("_DstBlend", (float)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
if (m.HasProperty("_ZWrite")) m.SetFloat("_ZWrite", 0f);
m.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
}
else if (m.HasProperty("_Mode"))
{
m.SetFloat("_Mode", 3f);
m.SetOverrideTag("RenderType", "Transparent");
if (m.HasProperty("_SrcBlend")) m.SetFloat("_SrcBlend", (float)UnityEngine.Rendering.BlendMode.SrcAlpha);
if (m.HasProperty("_DstBlend")) m.SetFloat("_DstBlend", (float)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
if (m.HasProperty("_ZWrite")) m.SetFloat("_ZWrite", 0f);
m.DisableKeyword("_ALPHATEST_ON");
m.EnableKeyword("_ALPHABLEND_ON");
m.DisableKeyword("_ALPHAPREMULTIPLY_ON");
m.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
}
else
{
m.SetOverrideTag("RenderType", "Transparent");
if (m.HasProperty("_SrcBlend")) m.SetFloat("_SrcBlend", (float)UnityEngine.Rendering.BlendMode.SrcAlpha);
if (m.HasProperty("_DstBlend")) m.SetFloat("_DstBlend", (float)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
if (m.HasProperty("_ZWrite")) m.SetFloat("_ZWrite", 0f);
m.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
}
if (m.HasProperty("_BaseColor"))
{
Color c = m.GetColor("_BaseColor");
c.a = alpha;
m.SetColor("_BaseColor", c);
}
if (m.HasProperty("_Color"))
{
Color c = m.GetColor("_Color");
c.a = alpha;
m.SetColor("_Color", c);
}
}
private void RestoreTransparency()
{
if (_transparentRenderers.Count == 0 && _originalSharedMaterialsByRenderer.Count == 0) return;
foreach (var r in _transparentRenderers)
{
if (r == null) continue;
if (_originalSharedMaterialsByRenderer.TryGetValue(r, out var shared))
{
r.sharedMaterials = shared;
}
}
_transparentRenderers.Clear();
_originalSharedMaterialsByRenderer.Clear();
}
bool IsPartOfGeneratedTopology(Transform moduleTransform)
{
Transform current = moduleTransform;
while (current != null)
{
if (current.name == "GeneratedTopology")
{
return true;
}
current = current.parent;
}
return false;
}
void OnDisable()
{
DeselectModule();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d35bf7a5ed4da4d708a643ace1b67073
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,854 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using TMPro;
public class PositionControlGizmo : MonoBehaviour
{
[Header("Gizmo Settings")]
public float arrowLength = 1.0f;
public float arrowHeadSize = 0.25f;
public float lineWidth = 0.04f;
public float clickableRadius = 0.5f;
public float screenSpaceClickThreshold = 30f;
[Header("Colors")]
public Color xAxisColor = Color.red;
public Color yAxisColor = Color.green;
public Color zAxisColor = Color.blue;
public Color hoverColor = Color.yellow;
public Color selectedColor = new Color(1f, 1f, 0f, 0.8f);
[Header("References")]
public Camera mainCamera;
public ModulePositionController positionController;
[Header("Placement")]
[Tooltip("If enabled, the gizmo is drawn at the selected module's visual center (renderer/collider bounds center), rather than transform.position.")]
public bool centerOnModuleBounds = true;
[Tooltip("World-space offset added to gizmo origin.")]
public Vector3 gizmoWorldOffset = Vector3.zero;
private ModuleBase targetModule;
private int hoveredAxis = -1;
private int previousHoveredAxis = -1;
private int selectedAxis = -1;
private Color[] previousColors = new Color[3];
private bool isDragging = false;
private bool gizmoWasClickedThisFrame = false;
private Vector3 axisDragStartGizmoOrigin;
private Vector3 axisDragStartModulePosition;
private Vector2 axisDragStartMousePosition;
[Header("Axis Drag Settings")]
public float axisDragSensitivity = 0.01f;
public bool WasGizmoClicked()
{
return gizmoWasClickedThisFrame;
}
public void RefreshGizmoVisibility()
{
UpdateGizmoVisibility();
}
private LineRenderer xAxisLine;
private LineRenderer yAxisLine;
private LineRenderer zAxisLine;
private GameObject gizmoContainer;
private Transform xAxisHead;
private Transform yAxisHead;
private Transform zAxisHead;
private static Mesh _coneMesh;
void Start()
{
if (mainCamera == null)
{
mainCamera = Camera.main;
if (mainCamera == null)
{
mainCamera = FindObjectOfType<Camera>();
}
}
if (positionController == null)
{
positionController = GetComponentInParent<ModulePositionController>();
if (positionController == null)
{
positionController = FindObjectOfType<ModulePositionController>();
}
if (positionController != null && positionController.gizmo == null)
{
positionController.gizmo = this;
}
}
previousColors[0] = xAxisColor;
previousColors[1] = yAxisColor;
previousColors[2] = zAxisColor;
FindGizmoLines();
}
void FindGizmoLines()
{
if (gizmoContainer != null)
{
return;
}
Transform existingContainer = this.transform.Find("PositionControlGizmo");
if (existingContainer != null)
{
gizmoContainer = existingContainer.gameObject;
}
else
{
gizmoContainer = new GameObject("PositionControlGizmo");
gizmoContainer.transform.SetParent(this.transform);
gizmoContainer.transform.localPosition = Vector3.zero;
gizmoContainer.transform.localRotation = Quaternion.identity;
gizmoContainer.transform.localScale = Vector3.one;
}
Transform xAxisTransform = gizmoContainer.transform.Find("XAxis");
if (xAxisTransform == null)
{
GameObject xAxisObj = new GameObject("XAxis");
xAxisObj.transform.SetParent(gizmoContainer.transform);
xAxisObj.transform.localPosition = Vector3.zero;
xAxisObj.transform.localRotation = Quaternion.identity;
xAxisObj.transform.localScale = Vector3.one;
xAxisLine = xAxisObj.AddComponent<LineRenderer>();
SetupLineRenderer(xAxisLine, xAxisColor);
}
else
{
xAxisLine = xAxisTransform.GetComponent<LineRenderer>();
if (xAxisLine == null)
{
xAxisLine = xAxisTransform.gameObject.AddComponent<LineRenderer>();
SetupLineRenderer(xAxisLine, xAxisColor);
}
else
{
SetupLineRenderer(xAxisLine, xAxisColor);
}
}
Transform yAxisTransform = gizmoContainer.transform.Find("YAxis");
if (yAxisTransform == null)
{
GameObject yAxisObj = new GameObject("YAxis");
yAxisObj.transform.SetParent(gizmoContainer.transform);
yAxisObj.transform.localPosition = Vector3.zero;
yAxisObj.transform.localRotation = Quaternion.identity;
yAxisObj.transform.localScale = Vector3.one;
yAxisLine = yAxisObj.AddComponent<LineRenderer>();
SetupLineRenderer(yAxisLine, yAxisColor);
}
else
{
yAxisLine = yAxisTransform.GetComponent<LineRenderer>();
if (yAxisLine == null)
{
yAxisLine = yAxisTransform.gameObject.AddComponent<LineRenderer>();
SetupLineRenderer(yAxisLine, yAxisColor);
}
else
{
SetupLineRenderer(yAxisLine, yAxisColor);
}
}
Transform zAxisTransform = gizmoContainer.transform.Find("ZAxis");
if (zAxisTransform == null)
{
GameObject zAxisObj = new GameObject("ZAxis");
zAxisObj.transform.SetParent(gizmoContainer.transform);
zAxisObj.transform.localPosition = Vector3.zero;
zAxisObj.transform.localRotation = Quaternion.identity;
zAxisObj.transform.localScale = Vector3.one;
zAxisLine = zAxisObj.AddComponent<LineRenderer>();
SetupLineRenderer(zAxisLine, zAxisColor);
}
else
{
zAxisLine = zAxisTransform.GetComponent<LineRenderer>();
if (zAxisLine == null)
{
zAxisLine = zAxisTransform.gameObject.AddComponent<LineRenderer>();
SetupLineRenderer(zAxisLine, zAxisColor);
}
else
{
SetupLineRenderer(zAxisLine, zAxisColor);
}
}
xAxisHead = EnsureArrowHead(xAxisLine != null ? xAxisLine.transform : null, "Head");
yAxisHead = EnsureArrowHead(yAxisLine != null ? yAxisLine.transform : null, "Head");
zAxisHead = EnsureArrowHead(zAxisLine != null ? zAxisLine.transform : null, "Head");
gizmoContainer.SetActive(false);
}
private Transform EnsureArrowHead(Transform axisTransform, string name)
{
if (axisTransform == null) return null;
Transform head = axisTransform.Find(name);
if (head == null)
{
var go = new GameObject(name);
go.transform.SetParent(axisTransform, false);
head = go.transform;
}
var mf = head.GetComponent<MeshFilter>();
if (mf == null) mf = head.gameObject.AddComponent<MeshFilter>();
var mr = head.GetComponent<MeshRenderer>();
if (mr == null) mr = head.gameObject.AddComponent<MeshRenderer>();
if (_coneMesh == null)
{
_coneMesh = CreateConeMesh(18);
}
mf.sharedMesh = _coneMesh;
if (axisTransform.TryGetComponent<LineRenderer>(out var lr) && lr.material != null)
{
mr.sharedMaterial = lr.material;
}
else
{
var mat = new Material(Shader.Find("Unlit/Color"));
mat.color = Color.white;
mr.sharedMaterial = mat;
}
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
mr.receiveShadows = false;
return head;
}
private static Mesh CreateConeMesh(int segments)
{
segments = Mathf.Clamp(segments, 6, 64);
var mesh = new Mesh();
mesh.name = "PositionControlGizmo_Cone";
int vertCount = segments + 2; // base ring + base center + tip
Vector3[] v = new Vector3[vertCount];
Vector3[] n = new Vector3[vertCount];
Vector2[] uv = new Vector2[vertCount];
int baseCenter = segments;
int tip = segments + 1;
float radius = 0.5f;
for (int i = 0; i < segments; i++)
{
float a = (i / (float)segments) * Mathf.PI * 2f;
float x = Mathf.Cos(a) * radius;
float y = Mathf.Sin(a) * radius;
v[i] = new Vector3(x, y, 0f);
n[i] = (new Vector3(x, y, radius)).normalized; // approximate
uv[i] = new Vector2(i / (float)segments, 0f);
}
v[baseCenter] = new Vector3(0f, 0f, 0f);
n[baseCenter] = Vector3.back;
uv[baseCenter] = new Vector2(0.5f, 0f);
v[tip] = new Vector3(0f, 0f, 1f);
n[tip] = Vector3.forward;
uv[tip] = new Vector2(0.5f, 1f);
int[] tris = new int[segments * 6];
int t = 0;
for (int i = 0; i < segments; i++)
{
int next = (i + 1) % segments;
tris[t++] = i;
tris[t++] = next;
tris[t++] = tip;
tris[t++] = next;
tris[t++] = i;
tris[t++] = baseCenter;
}
mesh.vertices = v;
mesh.normals = n;
mesh.uv = uv;
mesh.triangles = tris;
mesh.RecalculateBounds();
return mesh;
}
void OnEnable()
{
UpdateGizmoVisibility();
}
void UpdateGizmoVisibility()
{
if (gizmoContainer != null)
{
if (positionController != null)
{
targetModule = positionController.selectedModule;
}
bool shouldShow = targetModule != null && this.enabled;
if (shouldShow)
{
gizmoContainer.SetActive(true);
}
else
{
gizmoContainer.SetActive(false);
}
if (!shouldShow)
{
hoveredAxis = -1;
selectedAxis = -1;
isDragging = false;
}
}
else
{
FindGizmoLines();
}
}
void SetupLineRenderer(LineRenderer lr, Color color)
{
Shader unlitShader = Shader.Find("Unlit/Color");
Material gizmoMaterial;
if (unlitShader != null)
{
gizmoMaterial = new Material(unlitShader);
gizmoMaterial.color = color;
gizmoMaterial.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always);
gizmoMaterial.SetInt("_ZWrite", 0);
}
else
{
gizmoMaterial = new Material(Shader.Find("Sprites/Default"));
gizmoMaterial.color = color;
}
lr.material = gizmoMaterial;
lr.startColor = color;
lr.endColor = color;
lr.startWidth = lineWidth;
lr.endWidth = lineWidth;
lr.positionCount = 2;
lr.useWorldSpace = true;
lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
lr.receiveShadows = false;
lr.sortingOrder = 1000;
}
void UpdateGizmoVisualization()
{
if (targetModule == null)
{
return;
}
if (gizmoContainer == null)
{
FindGizmoLines();
if (gizmoContainer == null)
{
return;
}
}
if (xAxisLine == null || yAxisLine == null || zAxisLine == null)
{
FindGizmoLines();
if (xAxisLine == null || yAxisLine == null || zAxisLine == null)
{
return;
}
}
Vector3 gizmoOrigin = GetGizmoOrigin();
Color xColor = GetAxisColor(0);
if (previousColors[0] != xColor)
{
previousColors[0] = xColor;
}
UpdateAxisLine(xAxisLine, gizmoOrigin, GetAxisDirection(0), arrowLength, xColor);
UpdateAxisHead(xAxisHead, gizmoOrigin, GetAxisDirection(0), arrowLength, arrowHeadSize, xColor);
Color yColor = GetAxisColor(1);
if (previousColors[1] != yColor)
{
previousColors[1] = yColor;
}
UpdateAxisLine(yAxisLine, gizmoOrigin, GetAxisDirection(1), arrowLength, yColor);
UpdateAxisHead(yAxisHead, gizmoOrigin, GetAxisDirection(1), arrowLength, arrowHeadSize, yColor);
Color zColor = GetAxisColor(2);
if (previousColors[2] != zColor)
{
previousColors[2] = zColor;
}
UpdateAxisLine(zAxisLine, gizmoOrigin, GetAxisDirection(2), arrowLength, zColor);
UpdateAxisHead(zAxisHead, gizmoOrigin, GetAxisDirection(2), arrowLength, arrowHeadSize, zColor);
}
void UpdateAxisHead(Transform head, Vector3 start, Vector3 direction, float length, float headSize, Color color)
{
if (head == null) return;
Vector3 dir = direction.normalized;
Vector3 end = start + dir * length;
head.position = end;
head.rotation = Quaternion.LookRotation(dir, Vector3.up);
head.localScale = new Vector3(headSize, headSize, headSize);
// Update head material color (shared with line renderer in most cases)
var mr = head.GetComponent<MeshRenderer>();
if (mr != null && mr.sharedMaterial != null)
{
if (mr.sharedMaterial.HasProperty("_Color"))
mr.sharedMaterial.SetColor("_Color", color);
mr.sharedMaterial.color = color;
}
}
void UpdateAxisLine(LineRenderer lr, Vector3 start, Vector3 direction, float length, Color color)
{
if (lr == null)
{
return;
}
// Ensure LineRenderer is enabled
if (!lr.enabled)
{
lr.enabled = true;
}
Vector3 end = start + direction.normalized * length;
lr.SetPosition(0, start);
lr.SetPosition(1, end);
lr.startColor = color;
lr.endColor = color;
// Also update material color (important for Unlit/Color shader)
if (lr.material != null)
{
lr.material.color = color;
// Also set _Color property explicitly (some shaders use this)
if (lr.material.HasProperty("_Color"))
{
lr.material.SetColor("_Color", color);
}
}
}
void Update()
{
if (positionController != null)
{
targetModule = positionController.selectedModule;
}
UpdateGizmoVisibility();
if (targetModule != null)
{
HandleGizmoInteraction();
UpdateGizmoVisualization();
}
else
{
hoveredAxis = -1;
selectedAxis = -1;
isDragging = false;
}
}
void HandleGizmoInteraction()
{
if (targetModule == null || mainCamera == null)
{
hoveredAxis = -1;
return;
}
if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
{
hoveredAxis = -1;
return;
}
Vector3 gizmoOrigin = GetGizmoOrigin();
Vector2 mouseScreenPos = Input.mousePosition;
float closestDistance = float.MaxValue;
int closestAxis = -1;
for (int axis = 0; axis < 3; axis++)
{
Vector3 axisDirection = GetAxisDirection(axis);
Vector3 axisStart = gizmoOrigin;
Vector3 axisEnd = gizmoOrigin + axisDirection * arrowLength;
Vector3 screenStart3D = mainCamera.WorldToScreenPoint(axisStart);
Vector3 screenEnd3D = mainCamera.WorldToScreenPoint(axisEnd);
if (screenStart3D.z < 0 || screenEnd3D.z < 0)
continue;
Vector2 screenStart = new Vector2(screenStart3D.x, screenStart3D.y);
Vector2 screenEnd = new Vector2(screenEnd3D.x, screenEnd3D.y);
Vector2 closestPointOnLine = ClosestPointOnScreenLine(screenStart, screenEnd, mouseScreenPos);
float screenDistance = Vector2.Distance(mouseScreenPos, closestPointOnLine);
float distanceToStart = Vector2.Distance(mouseScreenPos, screenStart);
float minDistance = Mathf.Min(screenDistance, distanceToStart);
if (minDistance < screenSpaceClickThreshold && minDistance < closestDistance)
{
closestDistance = minDistance;
closestAxis = axis;
}
}
int oldHoveredAxis = hoveredAxis;
hoveredAxis = closestAxis;
if (hoveredAxis != oldHoveredAxis)
{
// no-op (hover state tracking only)
}
gizmoWasClickedThisFrame = false;
HandleAxisBasedInteraction();
}
void HandleAxisBasedInteraction()
{
if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
{
hoveredAxis = -1;
return;
}
Vector3 gizmoOrigin = GetGizmoOrigin();
Vector2 mouseScreenPos = Input.mousePosition;
if (Input.GetMouseButtonDown(0))
{
if (hoveredAxis >= 0)
{
selectedAxis = hoveredAxis;
isDragging = true;
axisDragStartGizmoOrigin = gizmoOrigin; // Store starting gizmo origin
axisDragStartModulePosition = targetModule.transform.position; // Store module pivot position for IK targets
axisDragStartMousePosition = mouseScreenPos; // Store starting mouse position
gizmoWasClickedThisFrame = true; // Mark that gizmo was clicked
if (positionController != null)
{
positionController.OnGizmoClicked();
}
}
}
else if (isDragging && selectedAxis >= 0 && Input.GetMouseButton(0))
{
ProcessUnityStyleAxisDrag();
}
else if (Input.GetMouseButtonUp(0))
{
if (isDragging)
{
isDragging = false;
selectedAxis = -1;
}
}
}
void ProcessUnityStyleAxisDrag()
{
if (targetModule == null || mainCamera == null || positionController == null) return;
Vector2 currentMousePos = Input.mousePosition;
Vector2 mouseDelta = currentMousePos - axisDragStartMousePosition;
float minMouseMovement = 2f; // Minimum pixels of movement required
if (mouseDelta.magnitude < minMouseMovement)
{
return; // User just clicked without dragging - no movement
}
Vector3 axisDirection = GetAxisDirection(selectedAxis);
Vector3 gizmoOrigin = axisDragStartGizmoOrigin; // Use gizmo origin as reference for screen-space projection
Vector2 screenAxis = (Vector2)mainCamera.WorldToScreenPoint(gizmoOrigin + axisDirection) -
(Vector2)mainCamera.WorldToScreenPoint(gizmoOrigin);
screenAxis.Normalize();
float movementAlongAxis = Vector2.Dot(mouseDelta, screenAxis);
float worldMovement = movementAlongAxis * axisDragSensitivity;
if (positionController != null && positionController.ikController != null)
{
worldMovement *= positionController.ikController.dragSensitivity;
}
// IMPORTANT: keep IK target based on module pivot (transform.position), not the visual gizmo origin.
Vector3 currentPosition = targetModule.transform.position;
Vector3 targetPosition = axisDragStartModulePosition + axisDirection * worldMovement;
float minMovementThreshold = 0.03f; // Minimum 3cm movement required
float distanceToTarget = Vector3.Distance(targetPosition, currentPosition);
if (distanceToTarget < minMovementThreshold)
{
return;
}
Vector3 intendedMovement = targetPosition - currentPosition;
float intendedAxisMovement = Vector3.Dot(intendedMovement, axisDirection);
float minAxisMovement = 0.02f; // Minimum 2cm movement along the selected axis
if (Mathf.Abs(intendedAxisMovement) < minAxisMovement)
{
return;
}
if (positionController.ikController != null)
{
bool success = positionController.ikController.SolveIK(targetPosition);
if (success)
{
Vector3 actualPosition = targetModule.transform.position;
Vector3 actualMovement = actualPosition - axisDragStartModulePosition;
float actualAxisMovement = Vector3.Dot(actualMovement, axisDirection);
float actualMovementDistance = actualMovement.magnitude;
if (actualMovementDistance >= minMovementThreshold && Mathf.Abs(actualAxisMovement) >= minAxisMovement)
{
if (Mathf.Sign(actualAxisMovement) == Mathf.Sign(intendedAxisMovement))
{
axisDragStartModulePosition = actualPosition;
axisDragStartGizmoOrigin = GetGizmoOrigin();
axisDragStartMousePosition = currentMousePos;
}
}
}
}
}
Vector3 GetGizmoOrigin()
{
if (targetModule == null) return Vector3.zero;
Vector3 origin = targetModule.transform.position;
if (!centerOnModuleBounds)
return origin + gizmoWorldOffset;
bool hasBounds = false;
Bounds b = default;
foreach (var r in GetRenderersOwnedByModule(targetModule))
{
if (r == null) continue;
if (!hasBounds)
{
b = r.bounds;
hasBounds = true;
}
else
{
b.Encapsulate(r.bounds);
}
}
if (!hasBounds)
{
foreach (var c in GetCollidersOwnedByModule(targetModule))
{
if (c == null) continue;
if (!hasBounds)
{
b = c.bounds;
hasBounds = true;
}
else
{
b.Encapsulate(c.bounds);
}
}
}
origin = hasBounds ? b.center : origin;
return origin + gizmoWorldOffset;
}
static System.Collections.Generic.IEnumerable<Renderer> GetRenderersOwnedByModule(ModuleBase rootModule)
{
if (rootModule == null) yield break;
Transform root = rootModule.transform;
var stack = new System.Collections.Generic.Stack<Transform>();
stack.Push(root);
while (stack.Count > 0)
{
Transform t = stack.Pop();
if (t == null) continue;
if (t != root)
{
ModuleBase other = t.GetComponent<ModuleBase>();
if (other != null && other != rootModule)
continue;
}
foreach (var r in t.GetComponents<Renderer>())
if (r != null) yield return r;
for (int i = 0; i < t.childCount; i++)
stack.Push(t.GetChild(i));
}
}
static System.Collections.Generic.IEnumerable<Collider> GetCollidersOwnedByModule(ModuleBase rootModule)
{
if (rootModule == null) yield break;
Transform root = rootModule.transform;
var stack = new System.Collections.Generic.Stack<Transform>();
stack.Push(root);
while (stack.Count > 0)
{
Transform t = stack.Pop();
if (t == null) continue;
if (t != root)
{
ModuleBase other = t.GetComponent<ModuleBase>();
if (other != null && other != rootModule)
continue;
}
foreach (var c in t.GetComponents<Collider>())
if (c != null) yield return c;
for (int i = 0; i < t.childCount; i++)
stack.Push(t.GetChild(i));
}
}
Vector2 ClosestPointOnScreenLine(Vector2 lineStart, Vector2 lineEnd, Vector2 point)
{
Vector2 line = lineEnd - lineStart;
float lineLength = line.magnitude;
if (lineLength < 0.001f)
return lineStart;
line.Normalize();
Vector2 pointToStart = point - lineStart;
float projectionLength = Vector2.Dot(pointToStart, line);
projectionLength = Mathf.Clamp(projectionLength, 0f, lineLength);
return lineStart + line * projectionLength;
}
void AdjustPosition(int axis, int sign)
{
if (positionController != null)
{
positionController.AdjustPositionAxis(axis, sign);
}
}
Vector3 GetAxisDirection(int axis)
{
switch (axis)
{
case 0: return Vector3.right; // X axis (world space - always points right)
case 1: return Vector3.up; // Y axis (world space - always points up)
case 2: return Vector3.forward; // Z axis (world space - always points forward)
default: return Vector3.zero;
}
}
Color GetAxisColor(int axis)
{
if (axis == selectedAxis) return selectedColor;
if (axis == hoveredAxis) return hoverColor;
switch (axis)
{
case 0: return xAxisColor;
case 1: return yAxisColor;
case 2: return zAxisColor;
default: return Color.white;
}
}
Vector3 ClosestPointOnLineSegment(Vector3 lineStart, Vector3 lineEnd, Vector3 point)
{
Vector3 line = lineEnd - lineStart;
float lineLength = line.magnitude;
line.Normalize();
Vector3 pointToStart = point - lineStart;
float projectionLength = Vector3.Dot(pointToStart, line);
projectionLength = Mathf.Clamp(projectionLength, 0f, lineLength);
return lineStart + line * projectionLength;
}
Vector3 ClosestPointOnRay(Ray ray, Vector3 point)
{
Vector3 rayToPoint = point - ray.origin;
float projectionLength = Vector3.Dot(rayToPoint, ray.direction);
projectionLength = Mathf.Max(0f, projectionLength); // Don't go backwards
return ray.origin + ray.direction * projectionLength;
}
void OnDestroy()
{
// Don't destroy gizmo container - it's part of the scene hierarchy, not created at runtime
// Just clear the reference
gizmoContainer = null;
xAxisLine = null;
yAxisLine = null;
zAxisLine = null;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7b174028420e641e388cafa6d51ffcf5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -6,9 +6,30 @@ public class ModuleSelector : MonoBehaviour
public Camera mainCamera;
public UserCameraControl cameraController;
[Header("Selection Camera Focus")]
[Tooltip("If enabled, selecting a module will rotate the camera to look at it.")]
public bool focusCameraOnSelection = true;
[Tooltip("When this view is active, camera focus-on-selection is suppressed (used for IK view). If not set, will auto-resolve Views/InverseKinematicsView.")]
public GameObject inverseKinematicsViewRoot;
[System.NonSerialized]
public ModuleBase prevModule;
void Awake()
{
// Auto-resolve the IK view root if not wired in the inspector.
if (inverseKinematicsViewRoot == null)
{
Transform views = GameObject.Find("Views")?.transform;
inverseKinematicsViewRoot =
views != null ? views.Find("InverseKinematicsView")?.gameObject : null;
if (inverseKinematicsViewRoot == null)
inverseKinematicsViewRoot = GameObject.Find("InverseKinematicsView");
}
}
void Update()
{
if (Input.GetMouseButtonDown(0))
@@ -34,7 +55,8 @@ public class ModuleSelector : MonoBehaviour
ServoMotorModule.selectedModule = servo;
// tell camera to look at module
if (cameraController != null)
bool ikViewActive = inverseKinematicsViewRoot != null && inverseKinematicsViewRoot.activeInHierarchy;
if (focusCameraOnSelection && !ikViewActive && cameraController != null)
cameraController.LookAtTarget(module.transform);
}
}

View File

@@ -1338,8 +1338,6 @@ RectTransform:
- {fileID: 520530388}
- {fileID: 589715995}
- {fileID: 882055579}
- {fileID: 1622654344}
- {fileID: 1557863855}
- {fileID: 395535431}
- {fileID: 910000010}
- {fileID: 1506978711}
@@ -3336,7 +3334,7 @@ RectTransform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_Father: {fileID: 910000004}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
@@ -3608,7 +3606,9 @@ Transform:
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Children:
- {fileID: 920000002}
- {fileID: 867709480}
m_Father: {fileID: 910000002}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &910000005
@@ -3639,9 +3639,180 @@ Transform:
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Children:
- {fileID: 910000017}
- {fileID: 910000020}
- {fileID: 910000023}
m_Father: {fileID: 910000002}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &910000016
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 910000017}
- component: {fileID: 910000018}
m_Layer: 0
m_Name: IKController
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &910000017
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 910000016}
serializedVersion: 2
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: 910000006}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &910000018
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 910000016}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: aed8b5469e22847cbab133f3f20750b2, type: 3}
m_Name:
m_EditorClassIdentifier:
maxIterations: 10
tolerance: 0.1
constrainAngles: 1
dragSensitivity: 0.7
movementSpeed: 0.3
useSmoothing: 1
drawDebugLines: 1
debugLineColor: {r: 1, g: 1, b: 0, a: 1}
--- !u!1 &910000019
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 910000020}
- component: {fileID: 910000021}
m_Layer: 0
m_Name: PositionController
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &910000020
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 910000019}
serializedVersion: 2
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: 910000006}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &910000021
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 910000019}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d35bf7a5ed4da4d708a643ace1b67073, type: 3}
m_Name:
m_EditorClassIdentifier:
mainCamera: {fileID: 0}
ikController: {fileID: 910000018}
moduleLayer:
serializedVersion: 2
m_Bits: 4294967295
positionStepSize: 0.1
positionChangeThreshold: 0.01
selectedColor: {r: 0, g: 0.8, b: 1, a: 1}
xPlusButton: {fileID: 0}
xMinusButton: {fileID: 0}
yPlusButton: {fileID: 0}
yMinusButton: {fileID: 0}
zPlusButton: {fileID: 0}
zMinusButton: {fileID: 0}
gizmo: {fileID: 910000024}
--- !u!1 &910000022
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 910000023}
- component: {fileID: 910000024}
m_Layer: 0
m_Name: PositionControlGizmo
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &910000023
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 910000022}
serializedVersion: 2
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: 910000006}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &910000024
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 910000022}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7b174028420e641e388cafa6d51ffcf5, type: 3}
m_Name:
m_EditorClassIdentifier:
arrowLength: 1
arrowHeadSize: 0.25
lineWidth: 0.04
clickableRadius: 0.5
screenSpaceClickThreshold: 30
axisDragSensitivity: 0.01
xAxisColor: {r: 1, g: 0, b: 0, a: 1}
yAxisColor: {r: 0, g: 1, b: 0, a: 1}
zAxisColor: {r: 0, g: 0, b: 1, a: 1}
hoverColor: {r: 1, g: 1, b: 0, a: 1}
selectedColor: {r: 1, g: 1, b: 0, a: 0.8}
mainCamera: {fileID: 0}
positionController: {fileID: 910000021}
--- !u!1 &910000007
GameObject:
m_ObjectHideFlags: 0
@@ -6330,7 +6501,7 @@ RectTransform:
- {fileID: 1673794417}
- {fileID: 1829395383}
- {fileID: 1382778229}
m_Father: {fileID: 431914656}
m_Father: {fileID: 920000002}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0}
m_AnchorMax: {x: 0.5, y: 0}
@@ -6770,7 +6941,7 @@ RectTransform:
- {fileID: 1448228082}
- {fileID: 1266399281}
- {fileID: 551864339}
m_Father: {fileID: 431914656}
m_Father: {fileID: 920000002}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
@@ -8305,6 +8476,109 @@ BoxCollider:
serializedVersion: 3
m_Size: {x: 0.002819935, y: 0.0036000002, z: 0.0050528883}
m_Center: {x: 0.000009967983, y: 7.777669e-12, z: 0.0024735613}
--- !u!1 &920000001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 920000002}
- component: {fileID: 920000003}
- component: {fileID: 920000004}
- component: {fileID: 920000005}
m_Layer: 5
m_Name: LiveViewCanvas
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &920000002
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 920000001}
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:
- {fileID: 1557863855}
- {fileID: 1622654344}
m_Father: {fileID: 910000004}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!223 &920000003
Canvas:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 920000001}
m_Enabled: 1
serializedVersion: 3
m_RenderMode: 0
m_Camera: {fileID: 0}
m_PlaneDistance: 100
m_PixelPerfect: 0
m_ReceivesEvents: 1
m_OverrideSorting: 0
m_OverridePixelPerfect: 0
m_SortingBucketNormalizedSize: 0
m_VertexColorAlwaysGammaSpace: 0
m_AdditionalShaderChannelsFlag: 25
m_UpdateRectTransformForStandalone: 0
m_SortingLayerID: 0
m_SortingOrder: 1
m_TargetDisplay: 0
--- !u!114 &920000004
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 920000001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UiScaleMode: 0
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
m_ReferenceResolution: {x: 800, y: 600}
m_ScreenMatchMode: 0
m_MatchWidthOrHeight: 0
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
--- !u!114 &920000005
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 920000001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreReversedGraphics: 1
m_BlockingObjects: 0
m_BlockingMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
@@ -8323,7 +8597,6 @@ SceneRoots:
- {fileID: 50616112}
- {fileID: 2105855092}
- {fileID: 1588135740}
- {fileID: 867709480}
- {fileID: 1720588295}
- {fileID: 2021810912}
- {fileID: 943453644}