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"));
}
}