using System; using System.Collections; using System.Linq; using UnityEngine; using UnityEngine.Events; /// /// Central manager for the ghost/preview drag-and-drop workflow. /// Supports both placing new blocks (SetupBlock) and repositioning /// already-placed scene objects (SetupExistingBlock). /// public class GhostManager : MonoBehaviour { private static GhostManager instance; public static GhostManager getInstance() => instance; // ------------------------------------------------------------------------- // Ghost state // ------------------------------------------------------------------------- private BlockData selectedBlockData; private GameObject ghostObject; private bool canPlace = false; [SerializeField] private UnityEvent onClearEvent; // Per-prefab config private bool alignToSurfaceNormal = true; private bool snapRotation = false; private float followSpeed = 20f; private float heightOffset = 0f; private Color validTint = new Color(0.5f, 1f, 0.5f, 1f); private Color invalidTint = new Color(1f, 0.5f, 0.5f, 1f); private Transform attachPoint; private IValidator validator; // Drag-frame state private bool isValidPlacement = true; private Vector3 currentSurfaceNormal = Vector3.up; private Vector3 currentSurfacePoint = Vector3.zero; private Vector3 rotationAxis = Vector3.up; private Renderer[] ghostRenderers; // Block component refs on the ghost private SocketContainer container; private JointBlock jointBlock; private Block block; private bool isDragged = false; private bool isJustPlaced = false; // ------------------------------------------------------------------------- // Existing-block editing state // ------------------------------------------------------------------------- private GameObject editingSourceObject; // the real scene object currently being edited private Vector3 editingOriginalPos; // transform snapshot for cancel/undo private Quaternion editingOriginalRot; private bool isEditingExisting; // ------------------------------------------------------------------------- // Movement strategy // ------------------------------------------------------------------------- private IGhostMovementStrategy currentStrategy = new FreeMoveStrategy(); public void SetMovementStrategy(IGhostMovementStrategy strategy) { currentStrategy = strategy ?? new FreeMoveStrategy(); } // ------------------------------------------------------------------------- // External dependencies // ------------------------------------------------------------------------- private RayInteractor rayInteractor; private SnapSystem snappingSystem; private Environment environment; private CommandHandler commandHandler; private JointPlacementHandler jointPlacementHandler; private SocketIterator socketIterator; private BaseInputProvider inputProvider; // ------------------------------------------------------------------------- // Unity lifecycle // ------------------------------------------------------------------------- private void Awake() { if (instance == null) instance = this; else Destroy(gameObject); } private void Start() { rayInteractor = RayInteractor.getInstance(); snappingSystem = SnapSystem.getInstance(); environment = Environment.getInstance(); commandHandler = CommandHandler.getInstance(); jointPlacementHandler = JointPlacementHandler.getInstance(); socketIterator = SocketIterator.getInstance(); inputProvider = InputProviderManager.getInstance()?.getCurrentInputProvider(); environment.onBlockAddedEvent.AddListener(placed => afterCreation(placed.gameObject)); inputProvider.OnPlace.AddListener(OnPlace); inputProvider.OnDraggingEnd.AddListener(OnPlaceWithDragging); inputProvider.OnRotate.AddListener(() => { if (HasActiveGhost()) ghostObject.transform.RotateAround( ghostObject.transform.position, rotationAxis, 90f); }); inputProvider.OnCancel.AddListener(ClearGhostData); } private void Update() { if (!HasActiveGhost()) return; UpdateSurfaceTracking(); snappingSystem.UpdateSockets(currentSurfacePoint, ghostObject.transform); SocketPoint targetSocket = snappingSystem.GetClosestSocket(); SocketPoint mySocket = socketIterator.GetCurrentActiveSocket(); bool compatible = targetSocket != null && (mySocket == null || targetSocket.CanAccept(mySocket)); var ctx = new GhostMovementContext( ghostTransform: ghostObject.transform, surfacePoint: currentSurfacePoint, surfaceNormal: currentSurfaceNormal, mySocket: mySocket, targetSocket: compatible ? targetSocket : null, attachPoint: attachPoint, followSpeed: followSpeed, heightOffset: heightOffset, alignToSurfaceNormal: alignToSurfaceNormal, snapRotation: snapRotation); currentStrategy.UpdateMovement(ref ctx); rotationAxis = ctx.RotationAxis; isValidPlacement = validator != null ? validator.IsValidPlacement(ghostObject) : true; UpdateVisualFeedback(isValidPlacement); } public Transform GhostTransform => ghostObject != null ? ghostObject.transform : null; // ------------------------------------------------------------------------- // Placement // ------------------------------------------------------------------------- public void setDrag(bool drag) => isDragged = drag; private void OnPlace() { if (InputModeManager.Is(InputMode.UI)) return; if (!HasActiveGhost()) return; if (isJustPlaced) return; snappingSystem.Clear(); if (isEditingExisting) CommitExistingBlock(); else PlaceBlock(); isJustPlaced = true; StartCoroutine(resetPlaced()); clearGhostData(); } public void ForcePlace() { if (!HasActiveGhost()) return; snappingSystem.Clear(); if (isEditingExisting) CommitExistingBlock(); else PlaceBlock(); clearGhostData(); } private IEnumerator resetPlaced() { yield return new WaitForSeconds(0.1f); isJustPlaced = false; } private void OnPlaceWithDragging() { if (!isDragged) return; OnPlace(); } /// Place a brand-new block via create command. private void PlaceBlock() { if (!isValidPlacement) return; if (block != null) commandHandler.createBlock( block.getBlockData()?.blockName, ghostObject.transform.position, ghostObject.transform.rotation); } /// /// Commit the repositioned existing block via a move command so undo works. /// The source object is re-shown at its new transform; the ghost is discarded. /// private void CommitExistingBlock() { if (!isValidPlacement) { RestoreMaterialAlpha(editingSourceObject); RestoreMaterialColor(editingSourceObject); SetLayerRecursively(editingSourceObject, LayerMask.NameToLayer("Interactable")); return; } if (editingSourceObject == null) return; commandHandler.moveBlock( editingSourceObject.GetComponent().getBlockId(), editingOriginalPos, ghostObject.transform.position, editingOriginalRot, ghostObject.transform.rotation); RestoreMaterialAlpha(editingSourceObject); RestoreMaterialColor(editingSourceObject); SetLayerRecursively(editingSourceObject, LayerMask.NameToLayer("Interactable")); environment.applyChanges(); // Null ghostObject BEFORE ClearGhostData runs so the else branch // has nothing to destroy ghostObject is the real object here ghostObject = null; editingSourceObject = null; isEditingExisting = false; } // ------------------------------------------------------------------------- // Public API new block // ------------------------------------------------------------------------- public void SetupBlock(BlockData blockData = null) { if (blockData == null && ghostObject != null) { if (!InputModeManager.Has(InputMode.ObjectDragging)) InputModeManager.Push(InputMode.ObjectDragging); ghostObject.SetActive(true); canPlace = true; return; } if (blockData != null) selectedBlockData = blockData; if (ghostObject != null) Destroy(ghostObject); if (selectedBlockData == null) { ghostObject = null; canPlace = false; return; } if (!InputModeManager.Has(InputMode.ObjectDragging)) InputModeManager.Push(InputMode.ObjectDragging); GameObject prefab = selectedBlockData.blockPrefab; ghostObject = Instantiate(prefab, rayInteractor.GetHitPosition(), prefab.transform.rotation); SetLayerRecursively(ghostObject, LayerMask.NameToLayer("Ignore Raycast")); InitialiseGhostFromObject(ghostObject); isEditingExisting = false; currentStrategy = new FreeMoveStrategy(); canPlace = true; } // ------------------------------------------------------------------------- // Public API existing block // ------------------------------------------------------------------------- /// /// Turns an already-placed scene object into an editable ghost in-place. /// The source object is hidden while editing; calling OnPlace commits the /// new transform via commandHandler.moveBlock so undo works. /// Calling ClearGhostData cancels and restores the original transform. /// /// The scene GameObject to reposition. /// /// Movement strategy to use. Defaults to FreeMoveStrategy if null. /// For a gizmo-style UI pass an AxisMoveStrategy or AxisRotateStrategy. /// public void SetupExistingBlock(GameObject sourceObject, IGhostMovementStrategy strategy = null) { if (sourceObject == null) return; if (ghostObject != null) Destroy(ghostObject); editingSourceObject = sourceObject; editingOriginalPos = sourceObject.transform.position; editingOriginalRot = sourceObject.transform.rotation; isEditingExisting = true; // Use the object itself as the ghost no cloning needed ghostObject = sourceObject; SetLayerRecursively(ghostObject, LayerMask.NameToLayer("Ignore Raycast")); InitialiseGhostFromObject(ghostObject); if (!InputModeManager.Has(InputMode.ObjectDragging)) InputModeManager.Push(InputMode.ObjectDragging); currentStrategy = strategy ?? new FreeMoveStrategy(); canPlace = true; } // ------------------------------------------------------------------------- // Ghost initialisation (shared between SetupBlock and SetupExistingBlock) // ------------------------------------------------------------------------- /// /// Reads GrabInteractable config, sets up sockets, renderers, and /// semi-transparent tint on the ghost object. /// private void InitialiseGhostFromObject(GameObject ghost) { GrabInteractable grabConfig = ghost.GetComponent(); if (grabConfig != null) { alignToSurfaceNormal = grabConfig.alignToSurfaceNormal; snapRotation = grabConfig.snapRotation; followSpeed = grabConfig.followSpeed; heightOffset = grabConfig.heightOffset; validTint = grabConfig.validTint; invalidTint = grabConfig.invalidTint; attachPoint = grabConfig.GetAttachPoint(); validator = grabConfig.GetValidator(); } else { alignToSurfaceNormal = true; snapRotation = false; followSpeed = 20f; heightOffset = 0f; validTint = new Color(0.5f, 1f, 0.5f, 1f); invalidTint = new Color(1f, 0.5f, 0.5f, 1f); validator = ghost.GetComponent(); } if (attachPoint == null) { attachPoint = new GameObject("attachPoint").transform; attachPoint.SetParent(ghost.transform); } jointBlock = ghost.GetComponent(); block = ghost.GetComponent(); container = ghost.GetComponent(); if (container != null && container.GetSocketCount() > 0) socketIterator.SetActiveContainer(container); else socketIterator.ClearActiveContainer(); ghostRenderers = ghost.GetComponentsInChildren().Where(x => x.gameObject.layer != LayerMask.NameToLayer("Gizmos")).ToArray(); foreach (Renderer r in ghostRenderers) foreach (Material mat in r.materials) { if (mat.HasProperty("_Color")) { mat.color = new Color(mat.color.r, mat.color.g, mat.color.b, 0.5f); } } } // ------------------------------------------------------------------------- // Shared helpers // ------------------------------------------------------------------------- public bool HasActiveGhost() => ghostObject != null && ghostObject.activeSelf && canPlace; public bool isGhost(GameObject obj) => obj == ghostObject; public void ClearGhost() { if (ghostObject != null) ghostObject.SetActive(false); canPlace = false; } public void ClearGhostData() { if (InputModeManager.Has(InputMode.ObjectDragging)) InputModeManager.Pop(InputMode.ObjectDragging); if (isEditingExisting) { // Only runs on cancel � CommitExistingBlock clears these before we get here if (editingSourceObject != null) { editingSourceObject.transform.SetPositionAndRotation( editingOriginalPos, editingOriginalRot); RestoreMaterialAlpha(editingSourceObject); SetLayerRecursively(editingSourceObject, LayerMask.NameToLayer("Interactable")); } } else { if (ghostObject != null) Destroy(ghostObject); } ghostObject = null; selectedBlockData = null; canPlace = false; editingSourceObject = null; isEditingExisting = false; snappingSystem?.Clear(); socketIterator?.ClearActiveContainer(); onClearEvent.Invoke(); } private void UpdateSurfaceTracking() { if (rayInteractor.GetHitInfo(out RaycastHit hitInfo)) { currentSurfacePoint = hitInfo.point; currentSurfaceNormal = hitInfo.normal.normalized; } else { currentSurfaceNormal = Vector3.up; } } private void UpdateVisualFeedback(bool isValid) { Color tint = isValid ? validTint : invalidTint; foreach (Renderer r in ghostRenderers) if (r != null) r.material.color = tint; } private static void SetLayerRecursively(GameObject obj, int layer, Func Method) { if (Method(obj.transform)) obj.layer = layer; foreach (Transform child in obj.transform) SetLayerRecursively(child.gameObject, layer, Method); } private static void SetLayerRecursively(GameObject obj, int layer) { SetLayerRecursively(obj, layer, x => { return x.gameObject.layer != LayerMask.NameToLayer("Gizmos"); }); } private static void RestoreMaterialAlpha(GameObject obj, Func Method) { foreach (Renderer r in obj.GetComponentsInChildren()) if (Method(r.gameObject)) { foreach (Material mat in r.materials) { if (mat.HasProperty("_Color")) { mat.color = new Color(mat.color.r, mat.color.g, mat.color.b, 1f); } } } } private static void RestoreMaterialAlpha(GameObject obj) { RestoreMaterialAlpha(obj, x => { return x.layer != LayerMask.NameToLayer("Gizmos"); }); } private static void RestoreMaterialColor(GameObject obj, Func Method) { foreach (Renderer r in obj.GetComponentsInChildren()) if (Method(r.gameObject)) { foreach (Material mat in r.materials) { if (mat.HasProperty("_Color")) { mat.color = new Color(1f, 1f, 1f, 1f); } } } } private static void RestoreMaterialColor(GameObject obj) { RestoreMaterialColor(obj, x => { return x.layer != LayerMask.NameToLayer("Gizmos"); }); } public void onClearAddListener(UnityAction action) => onClearEvent.AddListener(action); public void setupBlock(BlockData blockData = null) => SetupBlock(blockData); public void clearGhost() => ClearGhost(); public void clearGhostData() => ClearGhostData(); // ------------------------------------------------------------------------- // Post-placement coroutines (new blocks only) // ------------------------------------------------------------------------- private void afterCreation(GameObject blockObj) { if (blockObj == null) return; if (blockObj.GetComponent() == null && blockObj.layer != LayerMask.NameToLayer("Ignore Interaction")) blockObj.AddComponent(); SetLayerRecursively(blockObj, LayerMask.NameToLayer("Interactable")); } }