#if UNITY_EDITOR using System.Collections.Generic; using UnityEditor; using UnityEngine; namespace AutomaticDoorSystem.Editor { /// /// Edit-mode open/close preview for a DoorAuthoring, driven from its inspector. Replicates the /// EXACT math DoorAnimationSystem runs on the baked entities — including the parts that make a /// mis-pivoted door erratic (rotating doors treat quaternion.identity as the closed pose, slide /// offsets apply in the parent's frame) — so what you see here is what play mode will do, not /// what the gizmos suggest. One door previews at a time; the authored pose is restored on /// Reset, on deselection, and before play mode or a domain reload. /// public static class DoorPreviewDriver { private class PanelSnapshot { public Transform transform; public Vector3 localPosition; public Quaternion localRotation; public bool isLeft; } private static DoorAuthoring _door; private static readonly List _panels = new List(); private static double _animationStartTime; private static bool _animating; private static bool _isOpening; private static bool _directionForward = true; private static bool _poseIsOpen; static DoorPreviewDriver() { EditorApplication.playModeStateChanged += _ => EndPreview(); AssemblyReloadEvents.beforeAssemblyReload += EndPreview; // The swing-arc gizmos must stay put while a panel is being previewed: hand them the // authored (closed) rotation instead of the live one. DoorAuthoring.AuthoredWorldRotationProvider = AuthoredWorldRotation; DoorAuthoring.AuthoredWorldPositionProvider = AuthoredWorldPosition; } private static Vector3? AuthoredWorldPosition(Transform panel) { foreach (var snapshot in _panels) { if (snapshot.transform != panel) continue; return panel.parent != null ? panel.parent.TransformPoint(snapshot.localPosition) : snapshot.localPosition; } return null; } private static Quaternion? AuthoredWorldRotation(Transform panel) { foreach (var snapshot in _panels) { if (snapshot.transform != panel) continue; var parentRotation = panel.parent != null ? panel.parent.rotation : Quaternion.identity; return parentRotation * snapshot.localRotation; } return null; } public static bool IsPreviewing(DoorAuthoring door) => _door == door && _door != null; public static bool PoseIsOpen => _poseIsOpen; public static void PreviewOpen(DoorAuthoring door, bool directionForward = true) { if (Application.isPlaying || door == null || door.doorConfig == null) return; BeginSession(door); _directionForward = directionForward; _isOpening = true; _animationStartTime = EditorApplication.timeSinceStartup; _animating = true; } public static void PreviewClose(DoorAuthoring door) { if (Application.isPlaying || door == null || door.doorConfig == null) return; // Closing lerps from the OPEN pose (as the runtime does), so from a closed pose it // would snap the door open first — skip instead. if (IsPreviewing(door) && !_poseIsOpen && !_animating) return; BeginSession(door); _isOpening = false; _animationStartTime = EditorApplication.timeSinceStartup; _animating = true; } /// Restores the authored pose and ends the session. public static void EndPreview() { EditorApplication.update -= OnEditorUpdate; _animating = false; _poseIsOpen = false; foreach (var panel in _panels) { if (panel.transform == null) continue; panel.transform.localPosition = panel.localPosition; panel.transform.localRotation = panel.localRotation; } _panels.Clear(); _door = null; SceneView.RepaintAll(); } private static void BeginSession(DoorAuthoring door) { if (_door == door) return; EndPreview(); _door = door; var config = door.doorConfig; var isDouble = config.doorCount == DoorConfig.DoorCountEnum.Double; if (isDouble) { Snapshot(door.leftDoorMesh, true); Snapshot(door.rightDoorMesh, false); } else { Snapshot(door.doorMesh, false); } foreach (var panel in _panels) { Undo.RegisterCompleteObjectUndo(panel.transform, "Door Preview"); } EditorApplication.update += OnEditorUpdate; } private static void Snapshot(Transform panel, bool isLeft) { if (panel == null) return; _panels.Add(new PanelSnapshot { transform = panel, localPosition = panel.localPosition, localRotation = panel.localRotation, isLeft = isLeft }); } private static void OnEditorUpdate() { if (!_animating || _door == null || _door.doorConfig == null) { if (_door == null) EndPreview(); return; } var duration = Mathf.Max(_door.doorConfig.animationDuration, 0.01f); var progress = Mathf.Clamp01((float)(EditorApplication.timeSinceStartup - _animationStartTime) / duration); // Same cosine ease as DoorAnimationSystem.CalculateEasedProgress. var eased = -(Mathf.Cos(Mathf.PI * progress) - 1f) / 2f; ApplyPose(eased); if (progress >= 1f) { _animating = false; _poseIsOpen = _isOpening; // A finished Close stays where the runtime closes to (identity local rotation / // authored slide position). On a door whose authored rotation is off identity that // differs from the authored pose - restoring it here read as "the door jumped open". // The authored pose comes back when the door is deselected. } SceneView.RepaintAll(); } private static void ApplyPose(float easedProgress) { var config = _door.doorConfig; var isDouble = config.doorCount == DoorConfig.DoorCountEnum.Double; var isRotating = config.doorMovement == DoorConfig.DoorMovementEnum.Rotating; if (isRotating) { if (isDouble) ApplyRotatingDouble(config, easedProgress); else ApplyRotatingSingle(config, easedProgress); } else { if (isDouble && config.slidingStyle == DoorConfig.SlidingStyleEnum.Telescopic) ApplyTelescopicSliding(config, easedProgress); else ApplySliding(config, isDouble, easedProgress); } } // Mirrors DoorAnimationSystem.GetTargetRotation with DirectionForward from the preview toggle. private static void ApplyRotatingSingle(DoorConfig config, float easedProgress) { var forward = Quaternion.Euler(0f, config.openForwardAngle, 0f); var backward = Quaternion.Euler(0f, config.openBackwardAngle, 0f); Quaternion target; switch (config.openingStyle) { case DoorConfig.OpeningStyle.OneWay: var oneWayDir = config.oneWayDirection.sqrMagnitude > 0.0001f ? config.oneWayDirection.normalized : Vector3.forward; target = oneWayDir.z >= 0 ? backward : forward; break; default: // Forward and BothWay both pick by approach direction at runtime. target = _directionForward ? forward : backward; break; } foreach (var panel in _panels) { if (panel.transform == null) continue; panel.transform.localRotation = _isOpening ? Quaternion.Slerp(Quaternion.identity, target, easedProgress) : Quaternion.Slerp(target, Quaternion.identity, easedProgress); } } // Mirrors DoorAnimationSystem.GetDoubleRotations (note the runtime's forward/backward inversion // for the Forward style, and the mirrored right panel). private static void ApplyRotatingDouble(DoorConfig config, float easedProgress) { var forward = Quaternion.Euler(0f, config.openForwardAngle, 0f); var backward = Quaternion.Euler(0f, config.openBackwardAngle, 0f); Quaternion leftTarget; Quaternion rightTarget; switch (config.openingStyle) { case DoorConfig.OpeningStyle.BothWay: leftTarget = forward; rightTarget = forward; break; case DoorConfig.OpeningStyle.OneWay: var oneWayDir = config.oneWayDirection.sqrMagnitude > 0.0001f ? config.oneWayDirection.normalized : Vector3.forward; leftTarget = oneWayDir.z >= 0 ? backward : forward; rightTarget = Mirror(leftTarget); break; default: // Forward: runtime uses the BACKWARD rotation when approaching forward. leftTarget = _directionForward ? backward : forward; rightTarget = Mirror(leftTarget); break; } foreach (var panel in _panels) { if (panel.transform == null) continue; var target = panel.isLeft ? leftTarget : rightTarget; panel.transform.localRotation = _isOpening ? Quaternion.Slerp(Quaternion.identity, target, easedProgress) : Quaternion.Slerp(target, Quaternion.identity, easedProgress); } } private static Quaternion Mirror(Quaternion rotation) { var euler = rotation.eulerAngles; return Quaternion.Euler(euler.x, -euler.y, euler.z); } private static void ApplySliding(DoorConfig config, bool isDouble, float easedProgress) { foreach (var panel in _panels) { if (panel.transform == null) continue; var direction = !isDouble ? 1f : (panel.isLeft ? 1f : -1f); var offset = new Vector3( config.slideOpenOffset.x * direction, config.slideOpenOffset.y, config.slideOpenOffset.z); var closedPos = panel.localPosition; var openPos = closedPos + offset; panel.transform.localPosition = _isOpening ? Vector3.Lerp(closedPos, openPos, easedProgress) : Vector3.Lerp(openPos, closedPos, easedProgress); } } private static void ApplyTelescopicSliding(DoorConfig config, float easedProgress) { var leftSpan = config.slideOpenOffset.magnitude; var rightSpan = config.rightSlideOpenOffset.magnitude; DoorAnimationSystem.ComputeTelescopicTravel( rightSpan, leftSpan, config.openRightDoorOnly, easedProgress, _isOpening, out var rightTravel, out var leftTravel); foreach (var panel in _panels) { if (panel.transform == null) continue; Vector3 direction; float travel; if (panel.isLeft) { direction = leftSpan > 1e-4f ? config.slideOpenOffset / leftSpan : Vector3.zero; travel = leftTravel; } else { direction = rightSpan > 1e-4f ? config.rightSlideOpenOffset / rightSpan : Vector3.zero; travel = rightTravel; } panel.transform.localPosition = panel.localPosition + direction * travel; } } } } #endif