Files
ui/Assets/IK/ModulePositionController.cs
2026-02-22 19:48:28 -05:00

647 lines
20 KiB
C#

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("Fixed/Anchor Module (IK View)")]
[Tooltip("If enabled, the fixed (anchor) module is tinted red in IK view.")]
public bool makeFixedModuleRed = true;
public Color fixedModuleRedColor = Color.red;
[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();
private readonly Dictionary<Renderer, Material[]> _originalAnchorMaterialsByRenderer = new();
private readonly List<Renderer> _anchorRenderers = 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();
}
if (makeFixedModuleRed)
{
ApplyRedToAnchorModule();
}
}
public void DeselectModule()
{
RestoreTransparency();
RestoreAnchorColor();
if (selectedModule != null)
{
ServoMotorModule.selectedModule = null;
ObjectSelector.selectedObject = null;
}
selectedModule = null;
if (gizmo != null)
{
gizmo.enabled = false;
gizmo.RefreshGizmoVisibility();
}
}
private void ApplyTransparencyToSelectedModule()
{
RestoreTransparency();
RestoreAnchorColor();
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();
}
private void ApplyRedToAnchorModule()
{
RestoreAnchorColor();
if (ikController == null || selectedModule == null || !makeFixedModuleRed) return;
Transform anchor = ikController.GetAnchorPoint();
if (anchor == null || anchor == selectedModule.transform) return;
var anchorModule = anchor.GetComponent<ModuleBase>();
if (anchorModule == null) return;
foreach (var r in GetRenderersOwnedByModule(anchorModule))
{
if (!_originalAnchorMaterialsByRenderer.ContainsKey(r))
_originalAnchorMaterialsByRenderer[r] = r.sharedMaterials;
Material[] mats = r.materials;
for (int i = 0; i < mats.Length; i++)
{
if (mats[i] == null) continue;
ConfigureMaterialForRed(mats[i], fixedModuleRedColor);
}
_anchorRenderers.Add(r);
}
}
private static void ConfigureMaterialForRed(Material m, Color redColor)
{
if (m.HasProperty("_BaseColor"))
m.SetColor("_BaseColor", redColor);
if (m.HasProperty("_Color"))
m.SetColor("_Color", redColor);
}
private void RestoreAnchorColor()
{
if (_anchorRenderers.Count == 0 && _originalAnchorMaterialsByRenderer.Count == 0) return;
foreach (var r in _anchorRenderers)
{
if (r == null) continue;
if (_originalAnchorMaterialsByRenderer.TryGetValue(r, out var shared))
r.sharedMaterials = shared;
}
_anchorRenderers.Clear();
_originalAnchorMaterialsByRenderer.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();
}
}