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