Protfolio-Emanuel-Polsky / Assets / _Project / Code / VerletIntegration / Editor / CreatePoints.UI.cs
CreatePoints.UI.cs
Raw
using System;
using System.Collections.Generic;
using Unity.Physics;
using Unity.Physics.Authoring;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

namespace GarmentButton.VerletIntegration.CustomEdit
{
    public partial class CreatePoints
    {
        private VisualElement _root;
        private VisualElement _editingCard;
        private VisualElement _scenePreviewCard;
        private VisualElement _gridGenerationFields;
        private VisualElement _legendRow;
        private VisualElement _editMetricRow;
        private VisualElement _editSelectionLabel;
        private VisualElement _editSelectionRow;
        private VisualElement _editQuickEdgesLabel;
        private VisualElement _editQuickEdgesRow;
        private VisualElement _editActionsLabel;
        private VisualElement _editLockRow;
        private VisualElement _editUtilityRow;
        private VisualElement _generationMaintenanceGroup;
        private Button _createBoundButton;
        private Button _fitGroundButton;
        private Button _recalculateAllButton;
        private Button _recalculateSelectedButton;
        private Label _statsLabel;
        private HelpBox _modeHelpBox;
        private Label _editSummaryLabel;
        private Label _editModeHintLabel;
        private Label _selectedMetricLabel;
        private Label _selectedLockedMetricLabel;
        private Label _selectedFreeMetricLabel;
        private Label _previewSummaryLabel;

        private readonly List<Button> _pointActionButtons = new List<Button>();
        private readonly List<Button> _selectionActionButtons = new List<Button>();
        private readonly Dictionary<PointEditMode, ToolbarToggle> _editModeToggles = new Dictionary<PointEditMode, ToolbarToggle>();
        private readonly Dictionary<PreviewOverlay, ToolbarToggle> _previewToggles = new Dictionary<PreviewOverlay, ToolbarToggle>();
        private readonly Dictionary<PreviewOverlay, VisualElement> _previewChips = new Dictionary<PreviewOverlay, VisualElement>();

        private bool _drawSurface = true;
        private bool _drawSticks = true;
        private bool _drawBounds = true;
        private bool _drawHud = true;
        private bool _drawSelectedLabels = true;

        private readonly struct InspectorSnapshot
        {
            public readonly BakePoints Script;
            public readonly int PointCount;
            public readonly int StickCount;
            public readonly int TriangleCount;
            public readonly int LockedCount;
            public readonly int SelectedCount;
            public readonly int SelectedLockedCount;

            public InspectorSnapshot(
                BakePoints script,
                int pointCount,
                int stickCount,
                int triangleCount,
                int lockedCount,
                int selectedCount,
                int selectedLockedCount)
            {
                Script = script;
                PointCount = pointCount;
                StickCount = stickCount;
                TriangleCount = triangleCount;
                LockedCount = lockedCount;
                SelectedCount = selectedCount;
                SelectedLockedCount = selectedLockedCount;
            }

            public int SelectedFreeCount => Mathf.Max(0, SelectedCount - SelectedLockedCount);
            public bool HasPoints => PointCount > 0;
            public bool HasSticks => StickCount > 0;
            public bool HasSelection => SelectedCount > 0;
        }

        public override VisualElement CreateInspectorGUI()
        {
            ResetInspectorReferences();
            CacheMeshMode();

            _root = new VisualElement();
            _root.style.paddingLeft = 4f;
            _root.style.paddingRight = 4f;
            _root.style.paddingTop = 4f;
            _root.style.paddingBottom = 4f;

            SetUpEditPointInspector(_root);
            SetUpScenePreviewFields(_root);
            SetUpBakePointFields(_root);
            SetUpDefaultInspector(_root);
            DisableRelativeUIToMesh();
            RefreshInspectorState();

            return _root;
        }

        private void SetUpEditPointInspector(VisualElement root)
        {
            SetUpStatusCard(root);
            SetUpGenerationCard(root);
            SetUpEditingCard(root);
        }

        private void ResetInspectorReferences()
        {
            _root = null;
            _editingCard = null;
            _scenePreviewCard = null;
            _gridGenerationFields = null;
            _legendRow = null;
            _editMetricRow = null;
            _editSelectionLabel = null;
            _editSelectionRow = null;
            _editQuickEdgesLabel = null;
            _editQuickEdgesRow = null;
            _editActionsLabel = null;
            _editLockRow = null;
            _editUtilityRow = null;
            _generationMaintenanceGroup = null;
            _createBoundButton = null;
            _fitGroundButton = null;
            _recalculateAllButton = null;
            _recalculateSelectedButton = null;
            _statsLabel = null;
            _modeHelpBox = null;
            _editSummaryLabel = null;
            _editModeHintLabel = null;
            _selectedMetricLabel = null;
            _selectedLockedMetricLabel = null;
            _selectedFreeMetricLabel = null;
            _previewSummaryLabel = null;

            _pointActionButtons.Clear();
            _selectionActionButtons.Clear();
            _editModeToggles.Clear();
            _previewToggles.Clear();
            _previewChips.Clear();
        }

        private void ResetTransientSceneState()
        {
            _isBoxSelecting = false;
            _dragStart = Vector2.zero;
            _dragEnd = Vector2.zero;
            _selectedPoints.Clear();
            _worldPointCache = Array.Empty<Vector3>();
            _pointMaskCache = Array.Empty<bool>();
        }

        private void SetUpStatusCard(VisualElement root)
        {
            var card = CreateCard();
            card.style.borderLeftWidth = 3f;
            card.style.borderLeftColor = AccentColor();

            var header = new VisualElement();
            header.style.flexDirection = FlexDirection.Row;
            header.style.justifyContent = Justify.SpaceBetween;
            header.style.alignItems = Align.Center;
            header.style.marginBottom = 8f;

            var title = new Label("Verlet Points");
            title.style.unityFontStyleAndWeight = FontStyle.Bold;
            title.style.fontSize = 13f;

            _statsLabel = new Label();
            _statsLabel.style.unityTextAlign = TextAnchor.MiddleRight;
            _statsLabel.style.color = SecondaryTextColor();

            header.Add(title);
            header.Add(_statsLabel);

            _modeHelpBox = new HelpBox(string.Empty, HelpBoxMessageType.Info);
            _modeHelpBox.style.marginBottom = 8f;

            card.Add(header);
            card.Add(_modeHelpBox);
            _legendRow = CreateLegendRow();
            card.Add(_legendRow);
            root.Add(card);
        }

        private void SetUpGenerationCard(VisualElement root)
        {
            var card = CreateCard();
            card.Add(CreateSectionTitle("Generate"));

            _gridGenerationFields = new VisualElement();
            _gridGenerationFields.AddToClassList(GridModeOnlyClassName);
            _gridGenerationFields.Add(CreateSubsectionLabel("Grid"));
            AddPropertyField(_gridGenerationFields, nameof(BakePoints.Grid), "Grid", true);
            AddPropertyField(_gridGenerationFields, nameof(BakePoints.Distance), "Point Spacing", true);
            card.Add(_gridGenerationFields);

            var primaryRow = CreateButtonRow();
            var generateButton = CreateButton("Generate Free", CreateDots, "Build points from the mesh or grid handles with no preset locks.");
            StylePrimaryButton(generateButton);
            primaryRow.Add(generateButton);
            _createBoundButton = CreateButton("Create Bound", CreateBound, "Read the PhysicsShapeAuthoring box into the Verlet bounds.");
            primaryRow.Add(_createBoundButton);

            card.Add(primaryRow);

            var generationOptionsFoldout = new Foldout
            {
                text = "Optional Generation Tools",
                value = false
            };

            generationOptionsFoldout.Add(CreateSubsectionLabel("Preset Locks"));

            var presetRow = CreateButtonRow();
            var flagRacePresetButton = CreateButton("Flag Race Locks", () => CreateDots(PointLockPreset.FlagRaceSides), "Generate points and lock the side columns.");
            flagRacePresetButton.AddToClassList(GridOnlyPresetClassName);
            presetRow.Add(flagRacePresetButton);
            presetRow.Add(CreateButton("Hanging Cloth Locks", () => CreateDots(PointLockPreset.HangingClothTop), "Generate points and lock the top edge. In mesh mode this uses the mesh bounds."));

            generationOptionsFoldout.Add(presetRow);

            _generationMaintenanceGroup = new VisualElement();
            _generationMaintenanceGroup.Add(CreateSubsectionLabel("Maintenance"));

            var maintenanceRow = CreateButtonRow();
            var resetHandleButton = CreateButton("Reset Handles", ResetHandle, "Reset the grid width handles around this object.");
            resetHandleButton.AddToClassList(GridModeOnlyClassName);
            maintenanceRow.Add(resetHandleButton);
            _recalculateAllButton = CreateButton("Recalculate Lengths", RecalculateAllStickLengths, "Recalculate every stick length from the current point positions.");
            maintenanceRow.Add(_recalculateAllButton);
            _fitGroundButton = CreateButton("Fit Ground Bounds", FitBoundsToPhysicsGround, "Raycast the current bounds footprint against Unity Physics Shape authoring surfaces and move the lower bound to the nearest ground.");
            maintenanceRow.Add(_fitGroundButton);

            _generationMaintenanceGroup.Add(maintenanceRow);
            generationOptionsFoldout.Add(_generationMaintenanceGroup);
            card.Add(generationOptionsFoldout);
            root.Add(card);
        }

        private void SetUpEditingCard(VisualElement root)
        {
            var card = CreateCard();
            _editingCard = card;
            card.style.borderLeftWidth = 3f;
            card.style.borderLeftColor = SelectedPointColor;

            var header = new VisualElement();
            header.style.flexDirection = FlexDirection.Row;
            header.style.justifyContent = Justify.SpaceBetween;
            header.style.alignItems = Align.Center;
            header.style.marginBottom = 2f;

            _editSummaryLabel = new Label();
            _editSummaryLabel.style.color = SecondaryTextColor();
            _editSummaryLabel.style.unityTextAlign = TextAnchor.MiddleRight;

            header.Add(CreateSectionTitle("Edit"));
            header.Add(_editSummaryLabel);

            card.Add(header);

            var editModeToolbar = new Toolbar();
            editModeToolbar.tooltip = "None only previews points. Move drags points. Lock toggles points between locked and free.";
            editModeToolbar.style.marginBottom = 8f;
            editModeToolbar.style.minHeight = 30f;
            AddEditModeToggle(editModeToolbar, PointEditMode.None, "None");
            AddEditModeToggle(editModeToolbar, PointEditMode.Move, "Move");
            AddEditModeToggle(editModeToolbar, PointEditMode.Lock, "Lock");

            card.Add(CreateSubsectionLabel("Mode"));
            card.Add(editModeToolbar);

            _editModeHintLabel = CreateInlineStatusLabel();
            card.Add(_editModeHintLabel);

            var metricRow = CreateButtonRow();
            _editMetricRow = metricRow;
            metricRow.Add(CreateEditMetric("Selected", SelectedPointColor, out _selectedMetricLabel));
            metricRow.Add(CreateEditMetric("Locked", LockedPointColor, out _selectedLockedMetricLabel));
            metricRow.Add(CreateEditMetric("Free", FreePointColor, out _selectedFreeMetricLabel));
            card.Add(metricRow);

            var selectionRow = CreateButtonRow();
            _editSelectionRow = selectionRow;
            selectionRow.Add(CreatePointActionButton("Select All", SelectAllPoints, "Select every generated point."));
            selectionRow.Add(CreatePointActionButton("Select Locked", () => SelectPointsByLockState(true), "Select every locked point."));
            selectionRow.Add(CreatePointActionButton("Select Free", () => SelectPointsByLockState(false), "Select every unlocked point."));
            selectionRow.Add(CreatePointActionButton("Invert Selection", InvertSelection, "Invert the current point selection."));

            _editSelectionLabel = CreateSubsectionLabel("Selection");
            card.Add(_editSelectionLabel);
            card.Add(selectionRow);

            var regionRow = CreateButtonRow();
            _editQuickEdgesRow = regionRow;
            regionRow.Add(CreatePointActionButton("Top Edge", () => SelectPointsByRegion(PointSelectionRegion.TopEdge), "Select the upper edge of the current point layout."));
            regionRow.Add(CreatePointActionButton("Bottom Edge", () => SelectPointsByRegion(PointSelectionRegion.BottomEdge), "Select the lower edge of the current point layout."));
            regionRow.Add(CreatePointActionButton("Outer Border", () => SelectPointsByRegion(PointSelectionRegion.Border), "Select points on the outer border of the current point layout."));

            _editQuickEdgesLabel = CreateSubsectionLabel("Quick Edges");
            card.Add(_editQuickEdgesLabel);
            card.Add(regionRow);

            var lockRow = CreateButtonRow();
            _editLockRow = lockRow;
            var lockButton = CreateSelectionActionButton("Lock Selected", () => SetSelectedPointsLocked(true), "Lock all selected points.");
            StyleAccentButton(lockButton, LockedPointColor, true);
            lockRow.Add(lockButton);

            var unlockButton = CreateSelectionActionButton("Unlock Selected", () => SetSelectedPointsLocked(false), "Unlock all selected points.");
            StyleAccentButton(unlockButton, FreePointColor, false);
            lockRow.Add(unlockButton);

            var toggleButton = CreateSelectionActionButton("Toggle Selected", ToggleSelectedLocks, "Flip the lock state of every selected point.");
            StyleAccentButton(toggleButton, SelectedPointColor, false);
            lockRow.Add(toggleButton);

            var selectionUtilityRow = CreateButtonRow();
            _editUtilityRow = selectionUtilityRow;
            selectionUtilityRow.Add(CreateSelectionActionButton("Clear Selection", ClearSelection, "Clear selected Scene view points."));
            selectionUtilityRow.Add(CreateSelectionActionButton("Frame Selection", FrameSelectedPoints, "Frame selected points in the Scene view."));
            _recalculateSelectedButton = CreateSelectionActionButton("Recalc Selected", RecalculateSelectedStickLengths, "Recalculate stick lengths connected to selected points.");
            selectionUtilityRow.Add(_recalculateSelectedButton);

            _editActionsLabel = CreateSubsectionLabel("Actions");
            card.Add(_editActionsLabel);
            card.Add(lockRow);
            card.Add(selectionUtilityRow);
            root.Add(card);
        }

        private void SetUpBakePointFields(VisualElement root)
        {
            var settingsCard = CreateCard();
            settingsCard.style.marginTop = 6f;

            var foldout = new Foldout
            {
                text = "Optional Bake Settings",
                value = false
            };

            foldout.Add(CreateSubsectionLabel("Scene"));
            AddPropertyField(foldout, nameof(BakePoints.HandleSize), "Scene Handle Size");

            foldout.Add(CreateSubsectionLabel("Runtime"));
            AddPropertyField(foldout, nameof(BakePoints.FollowEntity), "Follow Entity");
            AddPropertyField(foldout, nameof(BakePoints.DebugCloth), "Debug Cloth");
            AddPropertyField(foldout, nameof(BakePoints.SimulationSettings), "Simulation Settings");

            foldout.Add(CreateSubsectionLabel("Bounds"));
            AddPropertyField(foldout, nameof(BakePoints.MinmumBound), "Minimum Bound");
            AddPropertyField(foldout, nameof(BakePoints.MaximumBound), "Maximum Bound");

            settingsCard.Add(foldout);
            root.Add(settingsCard);
        }

        private void SetUpScenePreviewFields(VisualElement root)
        {
            var previewCard = CreateCard();
            _scenePreviewCard = previewCard;
            previewCard.style.marginTop = 6f;
            previewCard.style.borderLeftWidth = 3f;
            previewCard.style.borderLeftColor = PreviewAccentColor();

            var foldout = new Foldout
            {
                text = "Scene Preview",
                value = false
            };

            var header = new VisualElement();
            header.style.flexDirection = FlexDirection.Row;
            header.style.justifyContent = Justify.SpaceBetween;
            header.style.alignItems = Align.Center;
            header.style.marginBottom = 2f;

            _previewSummaryLabel = new Label();
            _previewSummaryLabel.style.color = SecondaryTextColor();
            _previewSummaryLabel.style.unityTextAlign = TextAnchor.MiddleRight;

            header.Add(CreateSubsectionLabel("Overlays"));
            header.Add(_previewSummaryLabel);

            foldout.Add(header);

            var overlayRow = CreateButtonRow();
            overlayRow.Add(CreatePreviewChip(PreviewOverlay.Surface, "Surface", SurfacePreviewColor()));
            overlayRow.Add(CreatePreviewChip(PreviewOverlay.Sticks, "Sticks", StickPreviewColor()));
            overlayRow.Add(CreatePreviewChip(PreviewOverlay.Bounds, "Bounds", BoundPreviewColor()));
            overlayRow.Add(CreatePreviewChip(PreviewOverlay.Hud, "HUD", AccentColor()));
            overlayRow.Add(CreatePreviewChip(PreviewOverlay.Labels, "Labels", SelectedPointColor));

            foldout.Add(overlayRow);
            foldout.Add(CreateSubsectionLabel("Presets"));

            var presetRow = CreateButtonRow();
            presetRow.Add(CreateButton("Full Preview", () => SetPreviewPreset(true, true, true, true, true), "Show every Scene view overlay."));
            presetRow.Add(CreateButton("Clean Edit", () => SetPreviewPreset(false, true, true, true, false), "Hide the surface and point labels while keeping useful structure visible."));
            presetRow.Add(CreateButton("Points Only", () => SetPreviewPreset(false, false, false, false, false), "Hide all optional overlays and keep only point handles."));

            foldout.Add(presetRow);
            previewCard.Add(foldout);
            root.Add(previewCard);
        }

        private void SetUpDefaultInspector(VisualElement root)
        {
            var settingsCard = CreateCard();
            settingsCard.style.marginTop = 6f;

            var foldout = new Foldout
            {
                text = "Default Inspector",
                value = false
            };

            InspectorElement.FillDefaultInspector(foldout, serializedObject, this);
            settingsCard.Add(foldout);
            root.Add(settingsCard);
        }

        private void RefreshInspectorState()
        {
            if (_root == null)
                return;

            CacheMeshMode();

            var script = target as BakePoints;
            if (script != null)
                PruneSelection(script);

            var snapshot = CreateInspectorSnapshot(script);

            RefreshStatusLabels(snapshot);

            if (_editModeHintLabel != null)
                _editModeHintLabel.text = GetEditModeDescription(EditMode);

            RefreshProgressiveVisibility(snapshot);

            RefreshEditModeToggles();
            RefreshPreviewControls();

            if (_modeHelpBox != null)
            {
                if (_meshMode.HasMesh)
                {
                    _modeHelpBox.messageType = HelpBoxMessageType.Info;
                    _modeHelpBox.text = $"Mesh mode: Generate Points reads '{_meshMode.MeshName}'. Hanging Cloth Locks uses the mesh-local top edge.";
                }
                else if (_meshMode.HasFilter)
                {
                    _modeHelpBox.messageType = HelpBoxMessageType.Warning;
                    _modeHelpBox.text = "MeshFilter has no mesh. Grid mode is active until a mesh is assigned.";
                }
                else
                {
                    _modeHelpBox.messageType = HelpBoxMessageType.Info;
                    _modeHelpBox.text = "Grid mode: move the Start and End handles in the Scene view, then generate points.";
                }
            }

            DisableRelativeUIToMesh();
        }

        private InspectorSnapshot CreateInspectorSnapshot(BakePoints script)
        {
            var pointCount = script?.Points?.Length ?? 0;
            var stickCount = script?.Sticks?.Length ?? 0;
            var triangleCount = (script?.Tringles?.Length ?? 0) / 3;
            var lockedCount = CountLocked(script?.IsLocked);
            var selectedCount = CountValidSelectedPoints(script);
            var selectedLockedCount = CountSelectedLockedPoints(script);

            return new InspectorSnapshot(
                script,
                pointCount,
                stickCount,
                triangleCount,
                lockedCount,
                selectedCount,
                selectedLockedCount);
        }

        private void RefreshStatusLabels(InspectorSnapshot snapshot)
        {
            if (_statsLabel != null)
                _statsLabel.text = $"{snapshot.PointCount} pts | {snapshot.StickCount} sticks | {snapshot.TriangleCount} tris | {snapshot.LockedCount} locked";

            if (_editSummaryLabel != null)
                _editSummaryLabel.text = $"{snapshot.SelectedCount} selected";

            if (_selectedMetricLabel != null)
                _selectedMetricLabel.text = snapshot.SelectedCount.ToString();

            if (_selectedLockedMetricLabel != null)
                _selectedLockedMetricLabel.text = snapshot.SelectedLockedCount.ToString();

            if (_selectedFreeMetricLabel != null)
                _selectedFreeMetricLabel.text = snapshot.SelectedFreeCount.ToString();
        }

        private void RefreshProgressiveVisibility(InspectorSnapshot snapshot)
        {
            var editToolsVisible = snapshot.HasPoints && EditMode != PointEditMode.None;
            var selectionActionsVisible = editToolsVisible && snapshot.HasSelection;

            SetVisible(_legendRow, snapshot.HasPoints);
            SetVisible(_editingCard, snapshot.HasPoints);
            SetVisible(_scenePreviewCard, snapshot.HasPoints);
            SetVisible(_gridGenerationFields, !_meshMode.HasMesh);
            SetVisible(_createBoundButton, HasBoxShape(snapshot.Script));
            SetVisible(_generationMaintenanceGroup, !_meshMode.HasMesh || snapshot.HasSticks || HasUsableBounds(snapshot.Script));
            SetVisible(_recalculateAllButton, snapshot.HasSticks);
            SetVisible(_fitGroundButton, snapshot.HasPoints || HasUsableBounds(snapshot.Script));

            SetVisible(_editMetricRow, snapshot.HasSelection);
            SetVisible(_editSelectionLabel, editToolsVisible);
            SetVisible(_editSelectionRow, editToolsVisible);
            SetVisible(_editQuickEdgesLabel, editToolsVisible);
            SetVisible(_editQuickEdgesRow, editToolsVisible);
            SetVisible(_editActionsLabel, selectionActionsVisible);
            SetVisible(_editLockRow, selectionActionsVisible);
            SetVisible(_editUtilityRow, selectionActionsVisible);

            foreach (var button in _pointActionButtons)
                SetVisible(button, editToolsVisible);

            foreach (var button in _selectionActionButtons)
                SetVisible(button, selectionActionsVisible);

            SetVisible(_recalculateSelectedButton, selectionActionsVisible && snapshot.HasSticks);
        }

        private static void SetVisible(VisualElement element, bool visible)
        {
            if (element == null)
                return;

            element.SetEnabled(visible);
            element.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
        }

        private static bool HasBoxShape(BakePoints script)
        {
            if (script == null)
                return false;

            var shape = script.GetComponent<PhysicsShapeAuthoring>();
            return shape != null && shape.ShapeType == ShapeType.Box;
        }

        private static bool HasUsableBounds(BakePoints script)
        {
            return script != null && script.MaximumBound != script.MinmumBound;
        }

        private void DisableRelativeUIToMesh()
        {
            if (_root == null)
                return;

            SetVisibleForMeshState(GridModeOnlyClassName, !_meshMode.HasMesh);
            SetVisibleForMeshState(GridOnlyPresetClassName, !_meshMode.HasMesh);
        }

        private void SetVisibleForMeshState(string className, bool visible)
        {
            var elements = _root.Query<VisualElement>(className: className).Build();
            foreach (var element in elements)
            {
                element.SetEnabled(visible);
                element.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
            }
        }

        private void AddEditModeToggle(Toolbar toolbar, PointEditMode mode, string label)
        {
            var toggle = new ToolbarToggle
            {
                text = label,
                value = EditMode == mode
            };

            toggle.style.flexGrow = 1f;
            toggle.RegisterValueChangedCallback(evt =>
            {
                if (evt.newValue)
                {
                    SetEditMode(mode);
                    return;
                }

                if (EditMode == mode)
                    toggle.SetValueWithoutNotify(true);
            });

            _editModeToggles[mode] = toggle;
            StyleEditModeToggle(toggle, mode, EditMode == mode);
            toolbar.Add(toggle);
        }

        private void SetEditMode(PointEditMode mode)
        {
            if (EditMode == mode)
                return;

            EditMode = mode;
            _selectedPoints.Clear();
            RefreshEditModeToggles();
            RefreshInspectorState();
            ShowSceneNotification($"Edit mode: {mode}");
            SceneView.RepaintAll();
        }

        private void RefreshEditModeToggles()
        {
            foreach (var pair in _editModeToggles)
            {
                pair.Value.SetValueWithoutNotify(pair.Key == EditMode);
                StyleEditModeToggle(pair.Value, pair.Key, pair.Key == EditMode);
            }
        }

        private static void StyleEditModeToggle(ToolbarToggle toggle, PointEditMode mode, bool active)
        {
            var accent = GetEditModeColor(mode);
            toggle.style.unityFontStyleAndWeight = active ? FontStyle.Bold : FontStyle.Normal;
            toggle.style.backgroundColor = active ? ActiveOverlayColor(accent) : ButtonBackgroundColor();
            toggle.style.borderTopColor = active ? accent : ButtonBorderColor();
            toggle.style.borderRightColor = active ? accent : ButtonBorderColor();
            toggle.style.borderBottomColor = active ? accent : ButtonBorderColor();
            toggle.style.borderLeftColor = active ? accent : ButtonBorderColor();
            toggle.style.color = ButtonTextColor();
        }

        private static string GetEditModeDescription(PointEditMode mode)
        {
            switch (mode)
            {
                case PointEditMode.None:
                    return "Preview only. Point editing is paused.";
                case PointEditMode.Move:
                    return "Move mode. Drag one point, or drag a selected group together.";
                case PointEditMode.Lock:
                    return "Lock mode. Click a point to toggle its lock state.";
                default:
                    return string.Empty;
            }
        }

        private void SetPreviewPreset(bool surface, bool sticks, bool bounds, bool hud, bool labels)
        {
            _drawSurface = surface;
            _drawSticks = sticks;
            _drawBounds = bounds;
            _drawHud = hud;
            _drawSelectedLabels = labels;

            RefreshPreviewControls();
            SceneView.RepaintAll();
        }

        private void RefreshPreviewControls()
        {
            foreach (var pair in _previewToggles)
            {
                var value = GetPreviewOverlayValue(pair.Key);
                pair.Value.SetValueWithoutNotify(value);

                if (_previewChips.TryGetValue(pair.Key, out var chip))
                    StylePreviewChip(chip, value, GetPreviewOverlayColor(pair.Key));
            }

            if (_previewSummaryLabel != null)
                _previewSummaryLabel.text = $"{CountEnabledPreviewOverlays()} / 5 on";
        }

        private int CountEnabledPreviewOverlays()
        {
            var count = 0;
            if (_drawSurface) count++;
            if (_drawSticks) count++;
            if (_drawBounds) count++;
            if (_drawHud) count++;
            if (_drawSelectedLabels) count++;
            return count;
        }

        private bool GetPreviewOverlayValue(PreviewOverlay overlay)
        {
            switch (overlay)
            {
                case PreviewOverlay.Surface:
                    return _drawSurface;
                case PreviewOverlay.Sticks:
                    return _drawSticks;
                case PreviewOverlay.Bounds:
                    return _drawBounds;
                case PreviewOverlay.Hud:
                    return _drawHud;
                case PreviewOverlay.Labels:
                    return _drawSelectedLabels;
                default:
                    return false;
            }
        }

        private void SetPreviewOverlayValue(PreviewOverlay overlay, bool value)
        {
            switch (overlay)
            {
                case PreviewOverlay.Surface:
                    _drawSurface = value;
                    break;
                case PreviewOverlay.Sticks:
                    _drawSticks = value;
                    break;
                case PreviewOverlay.Bounds:
                    _drawBounds = value;
                    break;
                case PreviewOverlay.Hud:
                    _drawHud = value;
                    break;
                case PreviewOverlay.Labels:
                    _drawSelectedLabels = value;
                    break;
            }
        }

        private void AddPropertyField(VisualElement parent, string propertyName, string label, bool gridModeOnly = false)
        {
            var property = serializedObject.FindProperty(propertyName);
            if (property == null)
            {
                parent.Add(new HelpBox($"Missing serialized field: {propertyName}", HelpBoxMessageType.Warning));
                return;
            }

            var field = new PropertyField(property, label);
            field.style.marginBottom = 3f;

            if (gridModeOnly)
                field.AddToClassList(GridModeOnlyClassName);

            parent.Add(field);
        }

        private static Label CreateSectionTitle(string text)
        {
            var label = new Label(text);
            label.style.unityFontStyleAndWeight = FontStyle.Bold;
            label.style.fontSize = 13f;
            label.style.marginBottom = 2f;
            return label;
        }

        private static Label CreateSubsectionLabel(string text)
        {
            var label = new Label(text);
            label.style.unityFontStyleAndWeight = FontStyle.Bold;
            label.style.color = SecondaryTextColor();
            label.style.marginTop = 6f;
            label.style.marginBottom = 3f;
            return label;
        }

        private static Label CreateInlineStatusLabel()
        {
            var label = new Label();
            label.style.whiteSpace = WhiteSpace.Normal;
            label.style.color = SecondaryTextColor();
            label.style.marginTop = 0f;
            label.style.marginBottom = 8f;
            label.style.paddingLeft = 7f;
            label.style.paddingRight = 7f;
            label.style.paddingTop = 5f;
            label.style.paddingBottom = 5f;
            label.style.borderLeftWidth = 2f;
            label.style.borderLeftColor = SelectedPointColor;
            label.style.backgroundColor = ButtonBackgroundColor();
            return label;
        }

        private static VisualElement CreateEditMetric(string title, Color color, out Label valueLabel)
        {
            var metric = new VisualElement();
            metric.style.flexDirection = FlexDirection.Row;
            metric.style.alignItems = Align.Center;
            metric.style.flexGrow = 1f;
            metric.style.minWidth = 96f;
            metric.style.minHeight = 34f;
            metric.style.marginRight = 4f;
            metric.style.marginBottom = 4f;
            metric.style.paddingLeft = 7f;
            metric.style.paddingRight = 7f;
            metric.style.borderTopLeftRadius = 5f;
            metric.style.borderTopRightRadius = 5f;
            metric.style.borderBottomLeftRadius = 5f;
            metric.style.borderBottomRightRadius = 5f;
            metric.style.borderTopWidth = 1f;
            metric.style.borderRightWidth = 1f;
            metric.style.borderBottomWidth = 1f;
            metric.style.borderLeftWidth = 3f;
            metric.style.backgroundColor = ButtonBackgroundColor();
            metric.style.borderTopColor = ButtonBorderColor();
            metric.style.borderRightColor = ButtonBorderColor();
            metric.style.borderBottomColor = ButtonBorderColor();
            metric.style.borderLeftColor = color;

            valueLabel = new Label("0");
            valueLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
            valueLabel.style.fontSize = 14f;
            valueLabel.style.minWidth = 28f;
            valueLabel.style.color = ButtonTextColor();

            var textLabel = new Label(title);
            textLabel.style.color = SecondaryTextColor();
            textLabel.style.flexGrow = 1f;
            textLabel.style.unityTextAlign = TextAnchor.MiddleRight;

            metric.Add(valueLabel);
            metric.Add(textLabel);
            return metric;
        }

        private static VisualElement CreateLegendRow()
        {
            var row = new VisualElement();
            row.style.flexDirection = FlexDirection.Row;
            row.style.flexWrap = Wrap.Wrap;
            row.style.marginTop = 2f;

            row.Add(CreateLegendItem("Free", FreePointColor));
            row.Add(CreateLegendItem("Locked", LockedPointColor));
            row.Add(CreateLegendItem("Selected", SelectedPointColor));
            row.Add(CreateLegendItem("Sticks", StickColor));

            return row;
        }

        private static VisualElement CreateLegendItem(string label, Color color)
        {
            var item = new VisualElement();
            item.style.flexDirection = FlexDirection.Row;
            item.style.alignItems = Align.Center;
            item.style.marginRight = 12f;
            item.style.marginBottom = 2f;

            var swatch = new VisualElement();
            swatch.style.width = 9f;
            swatch.style.height = 9f;
            swatch.style.marginRight = 4f;
            swatch.style.borderTopLeftRadius = 4f;
            swatch.style.borderTopRightRadius = 4f;
            swatch.style.borderBottomLeftRadius = 4f;
            swatch.style.borderBottomRightRadius = 4f;
            swatch.style.backgroundColor = color;

            var text = new Label(label);
            text.style.color = SecondaryTextColor();

            item.Add(swatch);
            item.Add(text);
            return item;
        }

        private VisualElement CreatePreviewChip(PreviewOverlay overlay, string label, Color color)
        {
            var chip = new VisualElement();
            chip.style.flexDirection = FlexDirection.Row;
            chip.style.alignItems = Align.Center;
            chip.style.flexGrow = 1f;
            chip.style.minWidth = 116f;
            chip.style.minHeight = 30f;
            chip.style.marginRight = 4f;
            chip.style.marginBottom = 4f;
            chip.style.paddingLeft = 7f;
            chip.style.paddingRight = 6f;
            chip.style.borderTopLeftRadius = 6f;
            chip.style.borderTopRightRadius = 6f;
            chip.style.borderBottomLeftRadius = 6f;
            chip.style.borderBottomRightRadius = 6f;
            chip.style.borderTopWidth = 1f;
            chip.style.borderRightWidth = 1f;
            chip.style.borderBottomWidth = 1f;
            chip.style.borderLeftWidth = 1f;

            var swatch = new VisualElement();
            swatch.style.width = 8f;
            swatch.style.height = 18f;
            swatch.style.marginRight = 6f;
            swatch.style.borderTopLeftRadius = 4f;
            swatch.style.borderTopRightRadius = 4f;
            swatch.style.borderBottomLeftRadius = 4f;
            swatch.style.borderBottomRightRadius = 4f;
            swatch.style.backgroundColor = color;

            var toggle = new ToolbarToggle
            {
                text = label,
                value = GetPreviewOverlayValue(overlay)
            };
            toggle.style.flexGrow = 1f;
            toggle.style.minHeight = 22f;
            toggle.style.backgroundColor = Color.clear;
            toggle.style.borderTopWidth = 0f;
            toggle.style.borderRightWidth = 0f;
            toggle.style.borderBottomWidth = 0f;
            toggle.style.borderLeftWidth = 0f;
            toggle.RegisterValueChangedCallback(evt =>
            {
                SetPreviewOverlayValue(overlay, evt.newValue);
                StylePreviewChip(chip, evt.newValue, color);
                RefreshPreviewControls();
                SceneView.RepaintAll();
            });

            chip.Add(swatch);
            chip.Add(toggle);

            _previewToggles[overlay] = toggle;
            _previewChips[overlay] = chip;
            StylePreviewChip(chip, GetPreviewOverlayValue(overlay), color);

            return chip;
        }

        private static void StylePreviewChip(VisualElement chip, bool active, Color accent)
        {
            chip.style.backgroundColor = active ? ActiveOverlayColor(accent) : ButtonBackgroundColor();
            chip.style.borderTopColor = active ? accent : ButtonBorderColor();
            chip.style.borderRightColor = active ? accent : ButtonBorderColor();
            chip.style.borderBottomColor = active ? accent : ButtonBorderColor();
            chip.style.borderLeftColor = active ? accent : ButtonBorderColor();
        }

        private static VisualElement CreateCard()
        {
            var card = new VisualElement();
            card.style.paddingLeft = 10f;
            card.style.paddingRight = 10f;
            card.style.paddingTop = 10f;
            card.style.paddingBottom = 10f;
            card.style.marginBottom = 6f;
            card.style.borderTopLeftRadius = 6f;
            card.style.borderTopRightRadius = 6f;
            card.style.borderBottomLeftRadius = 6f;
            card.style.borderBottomRightRadius = 6f;
            card.style.borderTopWidth = 1f;
            card.style.borderRightWidth = 1f;
            card.style.borderBottomWidth = 1f;
            card.style.borderLeftWidth = 1f;
            card.style.backgroundColor = PanelColor();
            card.style.borderTopColor = BorderColor();
            card.style.borderRightColor = BorderColor();
            card.style.borderBottomColor = BorderColor();
            card.style.borderLeftColor = BorderColor();
            return card;
        }

        private static VisualElement CreateButtonRow()
        {
            var row = new VisualElement();
            row.style.flexDirection = FlexDirection.Row;
            row.style.flexWrap = Wrap.Wrap;
            row.style.marginBottom = 4f;
            return row;
        }

        private Button CreatePointActionButton(string text, Action clicked, string tooltip)
        {
            var button = CreateButton(text, clicked, tooltip);
            _pointActionButtons.Add(button);
            return button;
        }

        private Button CreateSelectionActionButton(string text, Action clicked, string tooltip)
        {
            var button = CreateButton(text, clicked, tooltip);
            _selectionActionButtons.Add(button);
            return button;
        }

        private static Button CreateButton(string text, Action clicked, string tooltip)
        {
            var button = new Button(clicked)
            {
                text = text,
                tooltip = tooltip
            };

            button.style.flexGrow = 1f;
            button.style.marginRight = 4f;
            button.style.marginBottom = 4f;
            button.style.minWidth = 96f;
            button.style.minHeight = 26f;
            button.style.paddingLeft = 8f;
            button.style.paddingRight = 8f;
            button.style.borderTopLeftRadius = 5f;
            button.style.borderTopRightRadius = 5f;
            button.style.borderBottomLeftRadius = 5f;
            button.style.borderBottomRightRadius = 5f;
            button.style.borderTopWidth = 1f;
            button.style.borderRightWidth = 1f;
            button.style.borderBottomWidth = 1f;
            button.style.borderLeftWidth = 1f;
            button.style.backgroundColor = ButtonBackgroundColor();
            button.style.borderTopColor = ButtonBorderColor();
            button.style.borderRightColor = ButtonBorderColor();
            button.style.borderBottomColor = ButtonBorderColor();
            button.style.borderLeftColor = ButtonBorderColor();
            button.style.color = ButtonTextColor();
            button.style.unityTextAlign = TextAnchor.MiddleCenter;
            return button;
        }

        private static void StylePrimaryButton(Button button)
        {
            button.style.unityFontStyleAndWeight = FontStyle.Bold;
            button.style.backgroundColor = AccentColor();
            button.style.borderTopColor = AccentColor();
            button.style.borderRightColor = AccentColor();
            button.style.borderBottomColor = AccentColor();
            button.style.borderLeftColor = AccentColor();
            button.style.color = Color.white;
        }

        private static void StyleAccentButton(Button button, Color accent, bool filled)
        {
            button.style.unityFontStyleAndWeight = FontStyle.Bold;
            button.style.backgroundColor = filled ? accent : ActiveOverlayColor(accent);
            button.style.borderTopColor = accent;
            button.style.borderRightColor = accent;
            button.style.borderBottomColor = accent;
            button.style.borderLeftColor = accent;
            button.style.color = filled ? Color.white : ButtonTextColor();
        }

        private static Color PanelColor()
        {
            return EditorGUIUtility.isProSkin
                ? new Color(0.16f, 0.17f, 0.18f, 1f)
                : new Color(0.86f, 0.88f, 0.9f, 1f);
        }

        private static Color BorderColor()
        {
            return EditorGUIUtility.isProSkin
                ? new Color(0.28f, 0.3f, 0.32f, 1f)
                : new Color(0.68f, 0.7f, 0.74f, 1f);
        }

        private static Color SecondaryTextColor()
        {
            return EditorGUIUtility.isProSkin
                ? new Color(0.72f, 0.74f, 0.77f, 1f)
                : new Color(0.28f, 0.3f, 0.34f, 1f);
        }

        private static Color AccentColor()
        {
            return EditorGUIUtility.isProSkin
                ? new Color(0.18f, 0.47f, 0.86f, 1f)
                : new Color(0.1f, 0.36f, 0.72f, 1f);
        }

        private static Color PreviewAccentColor()
        {
            return EditorGUIUtility.isProSkin
                ? new Color(0.24f, 0.64f, 0.72f, 1f)
                : new Color(0.08f, 0.46f, 0.56f, 1f);
        }

        private static Color ButtonBackgroundColor()
        {
            return EditorGUIUtility.isProSkin
                ? new Color(0.22f, 0.23f, 0.25f, 1f)
                : new Color(0.94f, 0.95f, 0.97f, 1f);
        }

        private static Color ButtonBorderColor()
        {
            return EditorGUIUtility.isProSkin
                ? new Color(0.34f, 0.36f, 0.39f, 1f)
                : new Color(0.66f, 0.69f, 0.74f, 1f);
        }

        private static Color ButtonTextColor()
        {
            return EditorGUIUtility.isProSkin
                ? new Color(0.9f, 0.92f, 0.95f, 1f)
                : new Color(0.12f, 0.14f, 0.18f, 1f);
        }

        private static Color SurfacePreviewColor()
        {
            return new Color(0.25f, 0.55f, 1f, 1f);
        }

        private static Color StickPreviewColor()
        {
            return EditorGUIUtility.isProSkin
                ? new Color(0.85f, 0.88f, 0.92f, 1f)
                : new Color(0.42f, 0.46f, 0.52f, 1f);
        }

        private static Color BoundPreviewColor()
        {
            return EditorGUIUtility.isProSkin
                ? new Color(0.95f, 0.95f, 0.95f, 1f)
                : new Color(0.32f, 0.34f, 0.38f, 1f);
        }

        private static Color GetPreviewOverlayColor(PreviewOverlay overlay)
        {
            switch (overlay)
            {
                case PreviewOverlay.Surface:
                    return SurfacePreviewColor();
                case PreviewOverlay.Sticks:
                    return StickPreviewColor();
                case PreviewOverlay.Bounds:
                    return BoundPreviewColor();
                case PreviewOverlay.Hud:
                    return AccentColor();
                case PreviewOverlay.Labels:
                    return SelectedPointColor;
                default:
                    return AccentColor();
            }
        }

        private static Color GetEditModeColor(PointEditMode mode)
        {
            switch (mode)
            {
                case PointEditMode.Move:
                    return GridHandleColor;
                case PointEditMode.Lock:
                    return LockedPointColor;
                case PointEditMode.None:
                default:
                    return SecondaryTextColor();
            }
        }

        private static Color ActiveOverlayColor(Color accent)
        {
            var amount = EditorGUIUtility.isProSkin ? 0.18f : 0.82f;
            var baseColor = ButtonBackgroundColor();
            return Color.Lerp(baseColor, accent, amount);
        }
    }
}