diff --git a/Assets/ControlLibrary/libc_control.dylib.meta b/Assets/ControlLibrary/libc_control.dylib.meta index ba21c78..fe3d27a 100644 --- a/Assets/ControlLibrary/libc_control.dylib.meta +++ b/Assets/ControlLibrary/libc_control.dylib.meta @@ -6,9 +6,9 @@ PluginImporter: iconMap: {} executionOrder: {} defineConstraints: [] - isPreloaded: 1 + isPreloaded: 0 isOverridable: 0 - isExplicitlyReferenced: 0 + isExplicitlyReferenced: 1 validateReferences: 1 platformData: - first: diff --git a/Assets/IK.meta b/Assets/IK.meta new file mode 100644 index 0000000..0920631 --- /dev/null +++ b/Assets/IK.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 748b1f7addcb948b49543a462fedf0fe +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/IK/InverseKinematicsController.cs b/Assets/IK/InverseKinematicsController.cs new file mode 100644 index 0000000..5cb9318 --- /dev/null +++ b/Assets/IK/InverseKinematicsController.cs @@ -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 kinematicChain = new List(); + 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(); + 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(); + if (module != null) + { + BatteryModule battery = child.GetComponent(); + HubModule hub = child.GetComponent(); + + if (battery != null || hub != null) + { + return child; + } + } + } + return null; + } + + private Transform FindEndModule(Transform topologyRoot) + { + List allModules = new List(); + foreach (Transform child in topologyRoot) + { + ModuleBase module = child.GetComponent(); + 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 allModules) + { + int count = 0; + + Transform currentParent = module.parent; + if (currentParent != null && currentParent.name != "GeneratedTopology") + { + ModuleBase parentModule = currentParent.GetComponent(); + if (parentModule != null) + { + count++; + } + else + { + Transform checkParent = currentParent.parent; + while (checkParent != null && checkParent.name != "GeneratedTopology") + { + ModuleBase checkModule = checkParent.GetComponent(); + 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; + } +} + diff --git a/Assets/IK/InverseKinematicsController.cs.meta b/Assets/IK/InverseKinematicsController.cs.meta new file mode 100644 index 0000000..7d11b35 --- /dev/null +++ b/Assets/IK/InverseKinematicsController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aed8b5469e22847cbab133f3f20750b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/IK/ModulePositionController.cs b/Assets/IK/ModulePositionController.cs new file mode 100644 index 0000000..58081ff --- /dev/null +++ b/Assets/IK/ModulePositionController.cs @@ -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 _originalSharedMaterialsByRenderer = new(); + private readonly List _transparentRenderers = new(); + + void Start() + { + if (mainCamera == null) + { + mainCamera = Camera.main; + if (mainCamera == null) + { + mainCamera = FindObjectOfType(); + } + } + + if (ikController == null) + { + ikController = GetComponentInParent(); + if (ikController == null) + { + ikController = FindObjectOfType(); + } + } + + if (gizmo == null) + { + gizmo = GetComponentInChildren(); + if (gizmo == null) + { + gizmo = FindObjectOfType(); + } + 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(); + } + 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(); + + 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(); + 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 GetRenderersOwnedByModule(ModuleBase rootModule) + { + if (rootModule == null) yield break; + + Transform root = rootModule.transform; + var stack = new Stack(); + stack.Push(root); + + while (stack.Count > 0) + { + Transform t = stack.Pop(); + if (t == null) continue; + + if (t != root) + { + ModuleBase other = t.GetComponent(); + if (other != null && other != rootModule) + continue; + } + + foreach (var r in t.GetComponents()) + { + 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(); + } +} diff --git a/Assets/IK/ModulePositionController.cs.meta b/Assets/IK/ModulePositionController.cs.meta new file mode 100644 index 0000000..0dcdac0 --- /dev/null +++ b/Assets/IK/ModulePositionController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d35bf7a5ed4da4d708a643ace1b67073 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/IK/PositionControlGizmo.cs b/Assets/IK/PositionControlGizmo.cs new file mode 100644 index 0000000..79f215a --- /dev/null +++ b/Assets/IK/PositionControlGizmo.cs @@ -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(); + } + } + + if (positionController == null) + { + positionController = GetComponentInParent(); + if (positionController == null) + { + positionController = FindObjectOfType(); + } + 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(); + SetupLineRenderer(xAxisLine, xAxisColor); + } + else + { + xAxisLine = xAxisTransform.GetComponent(); + if (xAxisLine == null) + { + xAxisLine = xAxisTransform.gameObject.AddComponent(); + 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(); + SetupLineRenderer(yAxisLine, yAxisColor); + } + else + { + yAxisLine = yAxisTransform.GetComponent(); + if (yAxisLine == null) + { + yAxisLine = yAxisTransform.gameObject.AddComponent(); + 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(); + SetupLineRenderer(zAxisLine, zAxisColor); + } + else + { + zAxisLine = zAxisTransform.GetComponent(); + if (zAxisLine == null) + { + zAxisLine = zAxisTransform.gameObject.AddComponent(); + 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(); + if (mf == null) mf = head.gameObject.AddComponent(); + + var mr = head.GetComponent(); + if (mr == null) mr = head.gameObject.AddComponent(); + + if (_coneMesh == null) + { + _coneMesh = CreateConeMesh(18); + } + mf.sharedMesh = _coneMesh; + + if (axisTransform.TryGetComponent(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(); + 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 GetRenderersOwnedByModule(ModuleBase rootModule) + { + if (rootModule == null) yield break; + Transform root = rootModule.transform; + + var stack = new System.Collections.Generic.Stack(); + stack.Push(root); + + while (stack.Count > 0) + { + Transform t = stack.Pop(); + if (t == null) continue; + + if (t != root) + { + ModuleBase other = t.GetComponent(); + if (other != null && other != rootModule) + continue; + } + + foreach (var r in t.GetComponents()) + if (r != null) yield return r; + + for (int i = 0; i < t.childCount; i++) + stack.Push(t.GetChild(i)); + } + } + + static System.Collections.Generic.IEnumerable GetCollidersOwnedByModule(ModuleBase rootModule) + { + if (rootModule == null) yield break; + Transform root = rootModule.transform; + + var stack = new System.Collections.Generic.Stack(); + stack.Push(root); + + while (stack.Count > 0) + { + Transform t = stack.Pop(); + if (t == null) continue; + + if (t != root) + { + ModuleBase other = t.GetComponent(); + if (other != null && other != rootModule) + continue; + } + + foreach (var c in t.GetComponents()) + 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; + } +} + diff --git a/Assets/IK/PositionControlGizmo.cs.meta b/Assets/IK/PositionControlGizmo.cs.meta new file mode 100644 index 0000000..1e718f1 --- /dev/null +++ b/Assets/IK/PositionControlGizmo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7b174028420e641e388cafa6d51ffcf5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ModuleSelector.cs b/Assets/ModuleSelector.cs index cc95dcc..5f5a5f0 100644 --- a/Assets/ModuleSelector.cs +++ b/Assets/ModuleSelector.cs @@ -5,10 +5,31 @@ 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); } } diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index 30b0d4e..9b9104d 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -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}