Shape and coloring
This commit is contained in:
@@ -47,7 +47,7 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
|
||||
_selectedSub = _bus.Subscribe<DrawingSelectedSignal>(OnDrawingSelected);
|
||||
_loadingScreen.Hide();
|
||||
}
|
||||
|
||||
|
||||
private void OnReturnToMainMenu(ReturnToMainMenuSignal signal)
|
||||
{
|
||||
LoadMainMenuSceneAsync().Forget(UnityEngine.Debug.LogException);
|
||||
@@ -70,8 +70,13 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
|
||||
|
||||
private async UniTaskVoid HandleSelectionAsync(string templateId)
|
||||
{
|
||||
_loadingScreen.Show();
|
||||
var progress = new Progress<float>(p => _loadingScreen.SetProgress(p * 0.5f));
|
||||
var mappedProgress = new Progress<float>(p => _loadingScreen.SetProgress(0.5f + p * 0.25f));
|
||||
await _progression.SetLastOpenedAsync(templateId);
|
||||
await _scenes.LoadSceneAsync(GameScene.Gameplay, progress: null, cancellationToken: default);
|
||||
await _scenes.LoadSceneAsync(nameof(GameScene.Gameplay), progress: progress, cancellationToken: default);
|
||||
await _scenes.UnloadSceneAsync(nameof(GameScene.Colorbook), progress: mappedProgress,
|
||||
cancellationToken: default);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -79,4 +84,4 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
|
||||
_selectedSub?.Dispose();
|
||||
_returnToMainMenuSubscription?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace Darkmatter.Features.Coloring.Systems;
|
||||
|
||||
public class ColoringController : IColoringController, IDisposable
|
||||
{
|
||||
private const string ColorButtonPrefabKey = "colorButton";
|
||||
private const string ColorButtonPrefabKey = "ColorButton";
|
||||
|
||||
private readonly ColoringStateRepository _repository;
|
||||
private readonly IColorButtonFactory _buttonFactory;
|
||||
@@ -67,7 +67,8 @@ public class ColoringController : IColoringController, IDisposable
|
||||
if (region == null) return;
|
||||
|
||||
var from = region.Color;
|
||||
_history.Push(new ColorRegionCommand(region, from, color));
|
||||
if (from != color)
|
||||
_history.Push(new ColorRegionCommand(region, from, color));
|
||||
_bus.Publish(new ColorAppliedSignal(regionId, color));
|
||||
}
|
||||
|
||||
@@ -84,7 +85,8 @@ public class ColoringController : IColoringController, IDisposable
|
||||
_history.Drop();
|
||||
|
||||
foreach (var button in _buttons)
|
||||
if (button != null) UnityEngine.Object.Destroy(button.gameObject);
|
||||
if (button != null)
|
||||
UnityEngine.Object.Destroy(button.gameObject);
|
||||
_buttons.Clear();
|
||||
|
||||
_regions.Clear();
|
||||
@@ -143,4 +145,4 @@ public class ColoringController : IColoringController, IDisposable
|
||||
foreach (var color in _repository.SelectedPalette.Colors)
|
||||
_buttons.Add(_buttonFactory.Create(_colorButtonPrefab, color));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,9 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
[RequireComponent(typeof(Button))]
|
||||
public class ColorButton : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Image swatch;
|
||||
[SerializeField] private GameObject selectedUI;
|
||||
[SerializeField] private Image[] swatches;
|
||||
[SerializeField] private GameObject activeUI;
|
||||
[SerializeField] private GameObject inactiveUI;
|
||||
private Button _button;
|
||||
private Color _color;
|
||||
private ColoringStateRepository _repository;
|
||||
@@ -26,7 +27,10 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
_color = color;
|
||||
_repository = repository;
|
||||
_sfx = sfx;
|
||||
swatch.color = color;
|
||||
foreach (var swatch in swatches)
|
||||
{
|
||||
swatch.color = color;
|
||||
}
|
||||
|
||||
_button.onClick.AddListener(OnClick);
|
||||
_repository.SelectedIndexChanged += UpdateSelectedUI;
|
||||
@@ -41,8 +45,10 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
|
||||
private void UpdateSelectedUI()
|
||||
{
|
||||
if (selectedUI != null)
|
||||
selectedUI.SetActive(_repository.SelectedColor == _color);
|
||||
if (activeUI != null)
|
||||
activeUI.SetActive(_repository.SelectedColor == _color);
|
||||
if (inactiveUI != null)
|
||||
inactiveUI.SetActive(_repository.SelectedColor != _color);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
@@ -51,4 +57,4 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
if (_repository != null) _repository.SelectedIndexChanged -= UpdateSelectedUI;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,18 +15,24 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
private CanvasGroup _canvasGroup;
|
||||
private Vector2 _shownAnchoredPos;
|
||||
private Sequence _activeSequence;
|
||||
private bool _refsReady;
|
||||
|
||||
public RectTransform SpawnRoot => spawnRoot;
|
||||
|
||||
private void Awake()
|
||||
private void Awake() => EnsureRefs();
|
||||
|
||||
private void EnsureRefs()
|
||||
{
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
if (_refsReady) return;
|
||||
if (_canvasGroup == null) _canvasGroup = GetComponent<CanvasGroup>();
|
||||
if (animatedRoot == null) animatedRoot = (RectTransform)transform;
|
||||
_shownAnchoredPos = animatedRoot.anchoredPosition;
|
||||
_refsReady = true;
|
||||
}
|
||||
|
||||
public Sequence Show()
|
||||
{
|
||||
EnsureRefs();
|
||||
KillActive();
|
||||
gameObject.SetActive(true);
|
||||
_canvasGroup.interactable = true;
|
||||
@@ -40,6 +46,7 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
|
||||
public Sequence Hide()
|
||||
{
|
||||
EnsureRefs();
|
||||
KillActive();
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
@@ -54,6 +61,7 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
|
||||
public void HideInstant()
|
||||
{
|
||||
EnsureRefs();
|
||||
KillActive();
|
||||
animatedRoot.anchoredPosition = _shownAnchoredPos + hiddenOffset;
|
||||
_canvasGroup.alpha = 0f;
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
|
||||
private Image _image;
|
||||
private event Action OnRegionClicked;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_image = GetComponent<Image>();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd1fab7418b3c40d09bf91807c6cefcc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,917 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Darkmatter.Core.Data.Static.Features.Coloring;
|
||||
using Darkmatter.Core.Data.Static.Features.DrawingTemplate;
|
||||
using Darkmatter.Core.Data.Static.Features.ShapeBuilder;
|
||||
using Darkmatter.Features.Coloring.UI;
|
||||
using Darkmatter.Features.ShapeBuilder.UI;
|
||||
using UnityEditor;
|
||||
using UnityEditor.AddressableAssets;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Features.DrawingTemplates.Editor
|
||||
{
|
||||
public sealed class DrawingTemplateEditorWindow : EditorWindow
|
||||
{
|
||||
private const string AddressableLabel = "drawing";
|
||||
private const string DrawingsGroupName = "Drawing";
|
||||
private const string PalettesGroupName = "Palettes";
|
||||
private const string DefaultTemplateFolder = "Assets/Darkmatter/Data/Drawings/Templates";
|
||||
private const string DefaultShapeFolder = "Assets/Darkmatter/Data/Drawings/Shapes";
|
||||
private const string DefaultPaletteFolder = "Assets/Darkmatter/Data/Drawings/Palettes";
|
||||
|
||||
private enum Tab { Templates, Shapes, Palettes }
|
||||
|
||||
private Tab _tab;
|
||||
private Vector2 _leftScroll;
|
||||
private Vector2 _rightScroll;
|
||||
private string _search = string.Empty;
|
||||
|
||||
private readonly List<DrawingTemplateSO> _templates = new();
|
||||
private readonly List<ShapeSO> _shapes = new();
|
||||
private readonly List<ColorPaletteSO> _palettes = new();
|
||||
|
||||
private DrawingTemplateSO _selectedTemplate;
|
||||
private ShapeSO _selectedShape;
|
||||
private ColorPaletteSO _selectedPalette;
|
||||
|
||||
private readonly List<string> _validation = new();
|
||||
private string _lastScanReport;
|
||||
private DefaultAsset _batchShapeFolder;
|
||||
private bool _autoAssignRegionIds = true;
|
||||
private bool _mergeRegions = false;
|
||||
|
||||
[MenuItem("Tools/Darkmatter/Drawing Template Editor", priority = 10)]
|
||||
public static void Open()
|
||||
{
|
||||
var win = GetWindow<DrawingTemplateEditorWindow>("Drawing Editor");
|
||||
win.minSize = new Vector2(900, 540);
|
||||
win.Refresh();
|
||||
}
|
||||
|
||||
public static void OpenAndSelect(DrawingTemplateSO template)
|
||||
{
|
||||
var win = GetWindow<DrawingTemplateEditorWindow>("Drawing Editor");
|
||||
win.minSize = new Vector2(900, 540);
|
||||
win.Refresh();
|
||||
win._tab = Tab.Templates;
|
||||
win.Select(template);
|
||||
}
|
||||
|
||||
private void OnEnable() => Refresh();
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
_templates.Clear();
|
||||
_shapes.Clear();
|
||||
_palettes.Clear();
|
||||
_templates.AddRange(FindAllOfType<DrawingTemplateSO>());
|
||||
_shapes.AddRange(FindAllOfType<ShapeSO>());
|
||||
_palettes.AddRange(FindAllOfType<ColorPaletteSO>());
|
||||
}
|
||||
|
||||
private static IEnumerable<T> FindAllOfType<T>() where T : ScriptableObject
|
||||
{
|
||||
var guids = AssetDatabase.FindAssets("t:" + typeof(T).Name);
|
||||
foreach (var g in guids)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(g);
|
||||
var a = AssetDatabase.LoadAssetAtPath<T>(path);
|
||||
if (a != null) yield return a;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
DrawToolbar();
|
||||
EditorGUILayout.Space(2);
|
||||
switch (_tab)
|
||||
{
|
||||
case Tab.Templates: DrawTemplatesTab(); break;
|
||||
case Tab.Shapes: DrawShapesTab(); break;
|
||||
case Tab.Palettes: DrawPalettesTab(); break;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Toolbar ──────────────────────────────────────────────────────────────
|
||||
private void DrawToolbar()
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||||
{
|
||||
if (TabButton("Templates", _tab == Tab.Templates)) _tab = Tab.Templates;
|
||||
if (TabButton("Shapes", _tab == Tab.Shapes)) _tab = Tab.Shapes;
|
||||
if (TabButton("Palettes", _tab == Tab.Palettes)) _tab = Tab.Palettes;
|
||||
GUILayout.FlexibleSpace();
|
||||
_search = GUILayout.TextField(_search, EditorStyles.toolbarSearchField, GUILayout.Width(220));
|
||||
if (GUILayout.Button("Refresh", EditorStyles.toolbarButton, GUILayout.Width(60)))
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TabButton(string label, bool active)
|
||||
{
|
||||
var bg = GUI.backgroundColor;
|
||||
if (active) GUI.backgroundColor = new Color(0.6f, 0.8f, 1f);
|
||||
var click = GUILayout.Button(label, EditorStyles.toolbarButton, GUILayout.Width(90));
|
||||
GUI.backgroundColor = bg;
|
||||
return click;
|
||||
}
|
||||
|
||||
// ─── Templates Tab ────────────────────────────────────────────────────────
|
||||
private void DrawTemplatesTab()
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
DrawTemplateList();
|
||||
DrawSeparator();
|
||||
DrawTemplateDetail();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTemplateList()
|
||||
{
|
||||
using (new EditorGUILayout.VerticalScope(GUILayout.Width(260)))
|
||||
{
|
||||
if (GUILayout.Button("+ New Drawing Template", GUILayout.Height(26)))
|
||||
CreateNewTemplate();
|
||||
EditorGUILayout.LabelField($"Templates ({_templates.Count})", EditorStyles.boldLabel);
|
||||
using (var scroll = new EditorGUILayout.ScrollViewScope(_leftScroll))
|
||||
{
|
||||
_leftScroll = scroll.scrollPosition;
|
||||
foreach (var t in _templates)
|
||||
{
|
||||
if (t == null) continue;
|
||||
if (!string.IsNullOrEmpty(_search) &&
|
||||
(t.name == null || t.name.IndexOf(_search, StringComparison.OrdinalIgnoreCase) < 0) &&
|
||||
(t.Id == null || t.Id.IndexOf(_search, StringComparison.OrdinalIgnoreCase) < 0))
|
||||
continue;
|
||||
DrawTemplateRow(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTemplateRow(DrawingTemplateSO t)
|
||||
{
|
||||
var active = _selectedTemplate == t;
|
||||
var bg = GUI.backgroundColor;
|
||||
if (active) GUI.backgroundColor = new Color(0.55f, 0.8f, 1f);
|
||||
using (new EditorGUILayout.HorizontalScope("box"))
|
||||
{
|
||||
if (t.DefaultThumbnail != null)
|
||||
GUILayout.Label(AssetPreview.GetAssetPreview(t.DefaultThumbnail), GUILayout.Width(36), GUILayout.Height(36));
|
||||
else
|
||||
GUILayout.Label(GUIContent.none, GUILayout.Width(36), GUILayout.Height(36));
|
||||
|
||||
using (new EditorGUILayout.VerticalScope())
|
||||
{
|
||||
GUILayout.Label(string.IsNullOrEmpty(t.DisplayName) ? t.name : t.DisplayName, EditorStyles.boldLabel);
|
||||
GUILayout.Label("id: " + (string.IsNullOrEmpty(t.Id) ? "<unset>" : t.Id), EditorStyles.miniLabel);
|
||||
}
|
||||
if (GUILayout.Button("Select", GUILayout.Width(54)))
|
||||
Select(t);
|
||||
}
|
||||
GUI.backgroundColor = bg;
|
||||
}
|
||||
|
||||
private void Select(DrawingTemplateSO t)
|
||||
{
|
||||
_selectedTemplate = t;
|
||||
_lastScanReport = null;
|
||||
Validate();
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void DrawTemplateDetail()
|
||||
{
|
||||
using (var scroll = new EditorGUILayout.ScrollViewScope(_rightScroll))
|
||||
{
|
||||
_rightScroll = scroll.scrollPosition;
|
||||
if (_selectedTemplate == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("Select a template from the left list, or click '+ New Drawing Template'.", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
var t = _selectedTemplate;
|
||||
var so = new SerializedObject(t);
|
||||
so.Update();
|
||||
|
||||
EditorGUILayout.LabelField(AssetDatabase.GetAssetPath(t), EditorStyles.miniLabel);
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
// Basic fields
|
||||
EditorGUILayout.LabelField("Basics", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(so.FindProperty("id"), new GUIContent("Id (Addressable Key)"));
|
||||
EditorGUILayout.PropertyField(so.FindProperty("displayName"));
|
||||
EditorGUILayout.PropertyField(so.FindProperty("defaultThumbnail"));
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
EditorGUILayout.LabelField("Prefabs", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(so.FindProperty("drawingPrefab"), new GUIContent("Drawing Prefab (SlotMarkers)"));
|
||||
EditorGUILayout.PropertyField(so.FindProperty("coloringPrefab"), new GUIContent("Coloring Prefab (ColorRegionViews)"));
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
EditorGUILayout.LabelField("Palette", EditorStyles.boldLabel);
|
||||
DrawPaletteIdField(so.FindProperty("colorPaletteId"));
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
EditorGUILayout.LabelField("Pieces", EditorStyles.boldLabel);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("Scan Drawing Prefab", GUILayout.Height(22)))
|
||||
{
|
||||
ScanDrawingPrefab(t);
|
||||
so.Update();
|
||||
}
|
||||
if (GUILayout.Button("Clear", GUILayout.Width(60)))
|
||||
{
|
||||
Undo.RecordObject(t, "Clear Pieces");
|
||||
var emptyPieces = new List<ShapeSO>();
|
||||
t.EditorSet(t.Id, t.DisplayName, t.DefaultThumbnail, t.DrawingPrefab, t.ColoringPrefab,
|
||||
emptyPieces, t.AuthoredRegions.ToList(), t.ColorPaletteId);
|
||||
EditorUtility.SetDirty(t);
|
||||
so.Update();
|
||||
}
|
||||
}
|
||||
EditorGUILayout.PropertyField(so.FindProperty("pieces"), includeChildren: true);
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
EditorGUILayout.LabelField("Regions", EditorStyles.boldLabel);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
_autoAssignRegionIds = GUILayout.Toggle(_autoAssignRegionIds,
|
||||
new GUIContent("Auto-assign blank Ids from GameObject name (writes back to prefab)"),
|
||||
GUILayout.ExpandWidth(false));
|
||||
}
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
_mergeRegions = GUILayout.Toggle(_mergeRegions,
|
||||
new GUIContent("Merge mode (keep existing entries' colors)"),
|
||||
GUILayout.ExpandWidth(false));
|
||||
}
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("Scan Coloring Prefab", GUILayout.Height(22)))
|
||||
{
|
||||
ScanColoringPrefab(t);
|
||||
so.Update();
|
||||
}
|
||||
if (GUILayout.Button("Clear", GUILayout.Width(60)))
|
||||
{
|
||||
Undo.RecordObject(t, "Clear Regions");
|
||||
t.EditorSet(t.Id, t.DisplayName, t.DefaultThumbnail, t.DrawingPrefab, t.ColoringPrefab,
|
||||
t.Pieces.ToList(), new List<RegionAuthoring>(), t.ColorPaletteId);
|
||||
EditorUtility.SetDirty(t);
|
||||
so.Update();
|
||||
}
|
||||
}
|
||||
EditorGUILayout.PropertyField(so.FindProperty("regions"), includeChildren: true);
|
||||
|
||||
so.ApplyModifiedProperties();
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
DrawValidationBox();
|
||||
EditorGUILayout.Space(4);
|
||||
if (!string.IsNullOrEmpty(_lastScanReport))
|
||||
EditorGUILayout.HelpBox(_lastScanReport, MessageType.Info);
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("Validate", GUILayout.Height(28)))
|
||||
Validate();
|
||||
if (GUILayout.Button("Mark Addressable ('drawing')", GUILayout.Height(28)))
|
||||
TryMakeAddressable(t);
|
||||
if (GUILayout.Button("Ping Asset", GUILayout.Width(120), GUILayout.Height(28)))
|
||||
EditorGUIUtility.PingObject(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPaletteIdField(SerializedProperty prop)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
EditorGUILayout.PropertyField(prop, new GUIContent("Palette Id"));
|
||||
var ids = _palettes.Where(p => p != null).Select(p => p.Id).Where(s => !string.IsNullOrEmpty(s)).Distinct().ToList();
|
||||
if (ids.Count == 0)
|
||||
{
|
||||
GUILayout.Label("(no palettes)", EditorStyles.miniLabel, GUILayout.Width(90));
|
||||
return;
|
||||
}
|
||||
var current = ids.IndexOf(prop.stringValue);
|
||||
var newIndex = EditorGUILayout.Popup(current, ids.ToArray(), GUILayout.Width(150));
|
||||
if (newIndex >= 0 && newIndex != current)
|
||||
prop.stringValue = ids[newIndex];
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Scans ────────────────────────────────────────────────────────────────
|
||||
private void ScanDrawingPrefab(DrawingTemplateSO t)
|
||||
{
|
||||
if (t.DrawingPrefab == null)
|
||||
{
|
||||
_lastScanReport = "No Drawing Prefab assigned.";
|
||||
return;
|
||||
}
|
||||
var path = AssetDatabase.GetAssetPath(t.DrawingPrefab);
|
||||
var root = PrefabUtility.LoadPrefabContents(path);
|
||||
try
|
||||
{
|
||||
var markers = root.GetComponentsInChildren<SlotMarker>(includeInactive: true);
|
||||
var pieces = new List<ShapeSO>();
|
||||
var missing = 0;
|
||||
foreach (var m in markers)
|
||||
{
|
||||
if (m.Shape == null) { missing++; continue; }
|
||||
if (!pieces.Contains(m.Shape)) pieces.Add(m.Shape);
|
||||
}
|
||||
Undo.RecordObject(t, "Scan Drawing Prefab");
|
||||
t.EditorSet(t.Id, t.DisplayName, t.DefaultThumbnail, t.DrawingPrefab, t.ColoringPrefab,
|
||||
pieces, t.AuthoredRegions.ToList(), t.ColorPaletteId);
|
||||
EditorUtility.SetDirty(t);
|
||||
_lastScanReport = $"Found {markers.Length} SlotMarker(s), {pieces.Count} unique ShapeSO. {missing} marker(s) had no ShapeSO assigned.";
|
||||
Validate();
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrefabUtility.UnloadPrefabContents(root);
|
||||
}
|
||||
}
|
||||
|
||||
private void ScanColoringPrefab(DrawingTemplateSO t)
|
||||
{
|
||||
if (t.ColoringPrefab == null)
|
||||
{
|
||||
_lastScanReport = "No Coloring Prefab assigned.";
|
||||
return;
|
||||
}
|
||||
var path = AssetDatabase.GetAssetPath(t.ColoringPrefab);
|
||||
var root = PrefabUtility.LoadPrefabContents(path);
|
||||
var existingColors = _mergeRegions
|
||||
? t.AuthoredRegions.ToDictionary(r => r.RegionId, r => r.InitialColor)
|
||||
: new Dictionary<string, Color>();
|
||||
try
|
||||
{
|
||||
var views = root.GetComponentsInChildren<ColorRegionView>(includeInactive: true);
|
||||
var regions = new List<RegionAuthoring>();
|
||||
var seen = new HashSet<string>();
|
||||
var assigned = new List<string>();
|
||||
var duplicates = new List<string>();
|
||||
var blanks = new List<string>();
|
||||
var prefabDirty = false;
|
||||
|
||||
foreach (var v in views)
|
||||
{
|
||||
var id = v.RegionId;
|
||||
var goPath = GetHierarchyPath(v.transform, root.transform);
|
||||
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
if (_autoAssignRegionIds)
|
||||
{
|
||||
var baseId = SanitizeId(v.gameObject.name);
|
||||
id = MakeUniqueId(baseId, seen);
|
||||
if (TrySetRegionId(v, id))
|
||||
{
|
||||
prefabDirty = true;
|
||||
assigned.Add($"{goPath} → '{id}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
blanks.Add($"{goPath} (could not set Id; backing field not found)");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
blanks.Add(goPath);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!seen.Add(id))
|
||||
{
|
||||
duplicates.Add($"{goPath} (id='{id}')");
|
||||
continue;
|
||||
}
|
||||
|
||||
var img = v.GetComponent<UnityEngine.UI.Image>();
|
||||
var col = existingColors.TryGetValue(id, out var keepCol)
|
||||
? keepCol
|
||||
: (img != null ? img.color : Color.white);
|
||||
regions.Add(new RegionAuthoring { RegionId = id, InitialColor = col });
|
||||
}
|
||||
|
||||
if (prefabDirty)
|
||||
PrefabUtility.SaveAsPrefabAsset(root, path);
|
||||
|
||||
Undo.RecordObject(t, "Scan Coloring Prefab");
|
||||
t.EditorSet(t.Id, t.DisplayName, t.DefaultThumbnail, t.DrawingPrefab, t.ColoringPrefab,
|
||||
t.Pieces.ToList(), regions, t.ColorPaletteId);
|
||||
EditorUtility.SetDirty(t);
|
||||
|
||||
var report = new System.Text.StringBuilder();
|
||||
report.AppendLine($"Found {views.Length} ColorRegionView(s). Stored {regions.Count}. Merge mode: {_mergeRegions}.");
|
||||
if (assigned.Count > 0)
|
||||
{
|
||||
report.AppendLine($"Auto-assigned {assigned.Count} blank Id(s) and wrote back to prefab:");
|
||||
foreach (var a in assigned) report.AppendLine(" • " + a);
|
||||
}
|
||||
if (blanks.Count > 0)
|
||||
{
|
||||
report.AppendLine($"Skipped {blanks.Count} blank Id(s) (enable auto-assign to fix):");
|
||||
foreach (var b in blanks) report.AppendLine(" • " + b);
|
||||
}
|
||||
if (duplicates.Count > 0)
|
||||
{
|
||||
report.AppendLine($"Skipped {duplicates.Count} duplicate Id(s):");
|
||||
foreach (var d in duplicates) report.AppendLine(" • " + d);
|
||||
}
|
||||
_lastScanReport = report.ToString().TrimEnd();
|
||||
Validate();
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrefabUtility.UnloadPrefabContents(root);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TrySetRegionId(ColorRegionView v, string id)
|
||||
{
|
||||
var vso = new SerializedObject(v);
|
||||
var prop = FindAutoProp(vso, "RegionId");
|
||||
if (prop == null) return false;
|
||||
prop.stringValue = id;
|
||||
vso.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorUtility.SetDirty(v);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Robust lookup for [field: SerializeField] backing fields across Unity versions.
|
||||
// Tries '<Name>k__BackingField', 'Name', 'name', 'm_Name', then iterates visible properties matching by name.
|
||||
private static SerializedProperty FindAutoProp(SerializedObject so, string propertyName)
|
||||
{
|
||||
var prop = so.FindProperty("<" + propertyName + ">k__BackingField");
|
||||
if (prop != null) return prop;
|
||||
prop = so.FindProperty(propertyName);
|
||||
if (prop != null) return prop;
|
||||
var camel = char.ToLowerInvariant(propertyName[0]) + propertyName.Substring(1);
|
||||
prop = so.FindProperty(camel);
|
||||
if (prop != null) return prop;
|
||||
prop = so.FindProperty("m_" + propertyName);
|
||||
if (prop != null) return prop;
|
||||
|
||||
var iter = so.GetIterator();
|
||||
if (!iter.NextVisible(true)) return null;
|
||||
do
|
||||
{
|
||||
if (iter.name.IndexOf(propertyName, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return iter.Copy();
|
||||
} while (iter.NextVisible(false));
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string MakeUniqueId(string baseId, HashSet<string> taken)
|
||||
{
|
||||
if (!taken.Contains(baseId)) return baseId;
|
||||
for (int i = 2; i < 10000; i++)
|
||||
{
|
||||
var candidate = baseId + "_" + i;
|
||||
if (!taken.Contains(candidate)) return candidate;
|
||||
}
|
||||
return baseId + "_" + System.Guid.NewGuid().ToString("N").Substring(0, 6);
|
||||
}
|
||||
|
||||
private static string GetHierarchyPath(Transform node, Transform root)
|
||||
{
|
||||
if (node == root) return node.name;
|
||||
var parts = new List<string> { node.name };
|
||||
var cur = node.parent;
|
||||
while (cur != null && cur != root)
|
||||
{
|
||||
parts.Add(cur.name);
|
||||
cur = cur.parent;
|
||||
}
|
||||
parts.Reverse();
|
||||
return string.Join("/", parts);
|
||||
}
|
||||
|
||||
// ─── Validation ───────────────────────────────────────────────────────────
|
||||
private void Validate()
|
||||
{
|
||||
_validation.Clear();
|
||||
var t = _selectedTemplate;
|
||||
if (t == null) return;
|
||||
if (string.IsNullOrWhiteSpace(t.Id)) _validation.Add("Id is empty.");
|
||||
if (string.IsNullOrWhiteSpace(t.DisplayName)) _validation.Add("DisplayName is empty.");
|
||||
if (t.DefaultThumbnail == null) _validation.Add("DefaultThumbnail is missing.");
|
||||
if (t.DrawingPrefab == null) _validation.Add("DrawingPrefab is missing.");
|
||||
if (t.ColoringPrefab == null) _validation.Add("ColoringPrefab is missing.");
|
||||
if (string.IsNullOrWhiteSpace(t.ColorPaletteId)) _validation.Add("ColorPaletteId is empty.");
|
||||
else if (!_palettes.Any(p => p != null && p.Id == t.ColorPaletteId))
|
||||
_validation.Add($"No ColorPaletteSO with Id '{t.ColorPaletteId}' found in project.");
|
||||
|
||||
if (t.Pieces == null || t.Pieces.Count == 0)
|
||||
_validation.Add("No Pieces assigned (use 'Scan Drawing Prefab').");
|
||||
else
|
||||
{
|
||||
var dup = t.Pieces.GroupBy(p => p != null ? p.Id : null).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
||||
foreach (var d in dup) _validation.Add($"Duplicate piece Id '{d}'.");
|
||||
if (t.Pieces.Any(p => p == null)) _validation.Add("A Piece slot is null.");
|
||||
if (t.Pieces.Any(p => p != null && string.IsNullOrEmpty(p.Id))) _validation.Add("A Piece has empty Id.");
|
||||
}
|
||||
|
||||
var ridDup = t.AuthoredRegions.GroupBy(r => r.RegionId).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
||||
foreach (var d in ridDup) _validation.Add($"Duplicate region Id '{d}'.");
|
||||
|
||||
// Cross-check: piece count vs slot count in DrawingPrefab
|
||||
if (t.DrawingPrefab != null && t.Pieces != null)
|
||||
{
|
||||
var path = AssetDatabase.GetAssetPath(t.DrawingPrefab);
|
||||
var root = PrefabUtility.LoadPrefabContents(path);
|
||||
try
|
||||
{
|
||||
var markerCount = root.GetComponentsInChildren<SlotMarker>(true).Length;
|
||||
if (markerCount != t.Pieces.Count)
|
||||
_validation.Add($"Pieces count ({t.Pieces.Count}) does not match SlotMarker count in prefab ({markerCount}).");
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawValidationBox()
|
||||
{
|
||||
if (_validation.Count == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox("Looks good.", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
EditorGUILayout.HelpBox(string.Join("\n", _validation.Select(v => "• " + v)), MessageType.Warning);
|
||||
}
|
||||
|
||||
// ─── Addressables ─────────────────────────────────────────────────────────
|
||||
private void TryMakeAddressable(UnityEngine.Object asset)
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
if (settings == null)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Addressables", "Addressables settings not found. Open Window > Asset Management > Addressables > Groups and click 'Create Addressables Settings' first.", "OK");
|
||||
return;
|
||||
}
|
||||
var group = EnsureGroup(settings, DrawingsGroupName);
|
||||
var path = AssetDatabase.GetAssetPath(asset);
|
||||
var guid = AssetDatabase.AssetPathToGUID(path);
|
||||
var entry = settings.CreateOrMoveEntry(guid, group);
|
||||
|
||||
var t = asset as DrawingTemplateSO;
|
||||
if (t != null && !string.IsNullOrEmpty(t.Id))
|
||||
entry.address = t.Id;
|
||||
|
||||
if (!settings.GetLabels().Contains(AddressableLabel))
|
||||
settings.AddLabel(AddressableLabel);
|
||||
entry.SetLabel(AddressableLabel, true, true, false);
|
||||
settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryModified, entry, true);
|
||||
_lastScanReport = $"Marked '{asset.name}' addressable in group '{group.Name}'. Address='{entry.address}', label='{AddressableLabel}'.";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Addressables", "Failed: " + e.Message, "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private static AddressableAssetGroup EnsureGroup(AddressableAssetSettings settings, string groupName)
|
||||
{
|
||||
var group = settings.FindGroup(groupName);
|
||||
if (group != null) return group;
|
||||
|
||||
System.Type[] schemaTypes;
|
||||
if (settings.DefaultGroup != null && settings.DefaultGroup.Schemas.Count > 0)
|
||||
schemaTypes = settings.DefaultGroup.Schemas.Select(s => s.GetType()).ToArray();
|
||||
else
|
||||
schemaTypes = new[] { typeof(UnityEditor.AddressableAssets.Settings.GroupSchemas.BundledAssetGroupSchema) };
|
||||
|
||||
return settings.CreateGroup(groupName, false, false, true, null, schemaTypes);
|
||||
}
|
||||
|
||||
// ─── New Template ─────────────────────────────────────────────────────────
|
||||
private void CreateNewTemplate()
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder(DefaultTemplateFolder))
|
||||
EnsureFolder(DefaultTemplateFolder);
|
||||
var path = EditorUtility.SaveFilePanelInProject(
|
||||
"New Drawing Template", "NewDrawing", "asset",
|
||||
"Choose where to save the drawing template asset.",
|
||||
DefaultTemplateFolder);
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
var t = CreateInstance<DrawingTemplateSO>();
|
||||
var name = Path.GetFileNameWithoutExtension(path);
|
||||
t.EditorSet(name, name, null, null, null, new List<ShapeSO>(), new List<RegionAuthoring>(), "defaultPalette");
|
||||
AssetDatabase.CreateAsset(t, path);
|
||||
AssetDatabase.SaveAssets();
|
||||
Refresh();
|
||||
Select(t);
|
||||
}
|
||||
|
||||
// ─── Shapes Tab ───────────────────────────────────────────────────────────
|
||||
private void DrawShapesTab()
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
using (new EditorGUILayout.VerticalScope(GUILayout.Width(280)))
|
||||
{
|
||||
if (GUILayout.Button("+ New Shape from Sprite", GUILayout.Height(24)))
|
||||
CreateShapePicker();
|
||||
|
||||
EditorGUILayout.LabelField("Batch import from folder:", EditorStyles.miniBoldLabel);
|
||||
_batchShapeFolder = (DefaultAsset)EditorGUILayout.ObjectField(_batchShapeFolder, typeof(DefaultAsset), false);
|
||||
using (new EditorGUI.DisabledScope(_batchShapeFolder == null))
|
||||
{
|
||||
if (GUILayout.Button("Batch Create From Folder"))
|
||||
BatchCreateShapesFromFolder(_batchShapeFolder);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.LabelField($"Shapes ({_shapes.Count})", EditorStyles.boldLabel);
|
||||
using (var s = new EditorGUILayout.ScrollViewScope(_leftScroll))
|
||||
{
|
||||
_leftScroll = s.scrollPosition;
|
||||
foreach (var sh in _shapes)
|
||||
{
|
||||
if (sh == null) continue;
|
||||
if (!string.IsNullOrEmpty(_search) &&
|
||||
sh.name.IndexOf(_search, StringComparison.OrdinalIgnoreCase) < 0 &&
|
||||
(sh.Id == null || sh.Id.IndexOf(_search, StringComparison.OrdinalIgnoreCase) < 0)) continue;
|
||||
DrawShapeRow(sh);
|
||||
}
|
||||
}
|
||||
}
|
||||
DrawSeparator();
|
||||
DrawShapeDetail();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawShapeRow(ShapeSO sh)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope("box"))
|
||||
{
|
||||
var preview = sh.Sprite != null ? AssetPreview.GetAssetPreview(sh.Sprite) : null;
|
||||
GUILayout.Label(preview, GUILayout.Width(36), GUILayout.Height(36));
|
||||
using (new EditorGUILayout.VerticalScope())
|
||||
{
|
||||
GUILayout.Label(sh.name, EditorStyles.boldLabel);
|
||||
GUILayout.Label("id: " + (string.IsNullOrEmpty(sh.Id) ? "<unset>" : sh.Id), EditorStyles.miniLabel);
|
||||
}
|
||||
if (GUILayout.Button("Edit", GUILayout.Width(48)))
|
||||
{
|
||||
_selectedShape = sh;
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawShapeDetail()
|
||||
{
|
||||
using (var s = new EditorGUILayout.ScrollViewScope(_rightScroll))
|
||||
{
|
||||
_rightScroll = s.scrollPosition;
|
||||
if (_selectedShape == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("Select a shape on the left, or create a new one.", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
var so = new SerializedObject(_selectedShape);
|
||||
so.Update();
|
||||
EditorGUILayout.LabelField(AssetDatabase.GetAssetPath(_selectedShape), EditorStyles.miniLabel);
|
||||
EditorGUILayout.PropertyField(FindAutoProp(so, "Id"), new GUIContent("Id"));
|
||||
EditorGUILayout.PropertyField(FindAutoProp(so, "Sprite"), new GUIContent("Sprite"));
|
||||
EditorGUILayout.PropertyField(FindAutoProp(so, "DefaultSizeDelta"), new GUIContent("Default Size Delta"));
|
||||
so.ApplyModifiedProperties();
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
if (_selectedShape.Sprite != null)
|
||||
{
|
||||
var preview = AssetPreview.GetAssetPreview(_selectedShape.Sprite);
|
||||
if (preview != null)
|
||||
GUILayout.Label(preview, GUILayout.Width(160), GUILayout.Height(160));
|
||||
}
|
||||
EditorGUILayout.Space(8);
|
||||
if (GUILayout.Button("Ping Asset", GUILayout.Width(120)))
|
||||
EditorGUIUtility.PingObject(_selectedShape);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateShapePicker()
|
||||
{
|
||||
var path = EditorUtility.OpenFilePanelWithFilters(
|
||||
"Pick sprite", Application.dataPath,
|
||||
new[] { "Sprites", "png,jpg,jpeg,psd,asset" });
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
if (!path.StartsWith(Application.dataPath))
|
||||
{
|
||||
EditorUtility.DisplayDialog("Pick sprite", "Sprite must be inside the project's Assets folder.", "OK");
|
||||
return;
|
||||
}
|
||||
var relative = "Assets" + path.Substring(Application.dataPath.Length);
|
||||
var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(relative);
|
||||
if (sprite == null)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Pick sprite", "Selected file is not a Sprite asset.", "OK");
|
||||
return;
|
||||
}
|
||||
CreateShape(sprite);
|
||||
}
|
||||
|
||||
private void BatchCreateShapesFromFolder(DefaultAsset folder)
|
||||
{
|
||||
var folderPath = AssetDatabase.GetAssetPath(folder);
|
||||
if (!AssetDatabase.IsValidFolder(folderPath))
|
||||
{
|
||||
EditorUtility.DisplayDialog("Batch import", "Selection is not a folder.", "OK");
|
||||
return;
|
||||
}
|
||||
var guids = AssetDatabase.FindAssets("t:Sprite", new[] { folderPath });
|
||||
int created = 0;
|
||||
foreach (var g in guids)
|
||||
{
|
||||
var p = AssetDatabase.GUIDToAssetPath(g);
|
||||
var spr = AssetDatabase.LoadAssetAtPath<Sprite>(p);
|
||||
if (spr == null) continue;
|
||||
CreateShape(spr, batch: true);
|
||||
created++;
|
||||
}
|
||||
AssetDatabase.SaveAssets();
|
||||
Refresh();
|
||||
_lastScanReport = $"Batch created {created} ShapeSO from '{folderPath}'.";
|
||||
}
|
||||
|
||||
private ShapeSO CreateShape(Sprite sprite, bool batch = false)
|
||||
{
|
||||
EnsureFolder(DefaultShapeFolder);
|
||||
var idGuess = SanitizeId(sprite.name);
|
||||
var path = $"{DefaultShapeFolder}/{idGuess}.asset";
|
||||
path = AssetDatabase.GenerateUniqueAssetPath(path);
|
||||
var shape = CreateInstance<ShapeSO>();
|
||||
var so = new SerializedObject(shape);
|
||||
FindAutoProp(so, "Id").stringValue = idGuess;
|
||||
FindAutoProp(so, "Sprite").objectReferenceValue = sprite;
|
||||
FindAutoProp(so, "DefaultSizeDelta").vector2Value = new Vector2(256, 256);
|
||||
so.ApplyModifiedProperties();
|
||||
AssetDatabase.CreateAsset(shape, path);
|
||||
if (!batch)
|
||||
{
|
||||
AssetDatabase.SaveAssets();
|
||||
Refresh();
|
||||
_selectedShape = shape;
|
||||
EditorGUIUtility.PingObject(shape);
|
||||
}
|
||||
return shape;
|
||||
}
|
||||
|
||||
// ─── Palettes Tab ─────────────────────────────────────────────────────────
|
||||
private void DrawPalettesTab()
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
using (new EditorGUILayout.VerticalScope(GUILayout.Width(260)))
|
||||
{
|
||||
if (GUILayout.Button("+ New Color Palette", GUILayout.Height(24)))
|
||||
CreatePalette();
|
||||
EditorGUILayout.LabelField($"Palettes ({_palettes.Count})", EditorStyles.boldLabel);
|
||||
using (var s = new EditorGUILayout.ScrollViewScope(_leftScroll))
|
||||
{
|
||||
_leftScroll = s.scrollPosition;
|
||||
foreach (var p in _palettes)
|
||||
{
|
||||
if (p == null) continue;
|
||||
using (new EditorGUILayout.HorizontalScope("box"))
|
||||
{
|
||||
using (new EditorGUILayout.VerticalScope())
|
||||
{
|
||||
GUILayout.Label(p.name, EditorStyles.boldLabel);
|
||||
GUILayout.Label("id: " + (string.IsNullOrEmpty(p.Id) ? "<unset>" : p.Id), EditorStyles.miniLabel);
|
||||
DrawPaletteSwatches(p);
|
||||
}
|
||||
if (GUILayout.Button("Edit", GUILayout.Width(48)))
|
||||
{
|
||||
_selectedPalette = p;
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DrawSeparator();
|
||||
using (var s = new EditorGUILayout.ScrollViewScope(_rightScroll))
|
||||
{
|
||||
_rightScroll = s.scrollPosition;
|
||||
if (_selectedPalette == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("Select a palette on the left, or create a new one.", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
var so = new SerializedObject(_selectedPalette);
|
||||
so.Update();
|
||||
EditorGUILayout.LabelField(AssetDatabase.GetAssetPath(_selectedPalette), EditorStyles.miniLabel);
|
||||
EditorGUILayout.PropertyField(FindAutoProp(so, "Id"), new GUIContent("Id"));
|
||||
EditorGUILayout.PropertyField(FindAutoProp(so, "Colors"), new GUIContent("Colors"), includeChildren: true);
|
||||
so.ApplyModifiedProperties();
|
||||
EditorGUILayout.Space(6);
|
||||
if (GUILayout.Button("Mark Addressable (key=Id)", GUILayout.Height(24)))
|
||||
TryMakeAddressablePalette(_selectedPalette);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TryMakeAddressablePalette(ColorPaletteSO p)
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
if (settings == null)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Addressables", "Addressables settings not found. Open Window > Asset Management > Addressables > Groups and click 'Create Addressables Settings' first.", "OK");
|
||||
return;
|
||||
}
|
||||
var group = EnsureGroup(settings, PalettesGroupName);
|
||||
var path = AssetDatabase.GetAssetPath(p);
|
||||
var guid = AssetDatabase.AssetPathToGUID(path);
|
||||
var entry = settings.CreateOrMoveEntry(guid, group);
|
||||
if (!string.IsNullOrEmpty(p.Id)) entry.address = p.Id;
|
||||
settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryModified, entry, true);
|
||||
_lastScanReport = $"Marked palette '{p.name}' addressable in group '{group.Name}'. Address='{entry.address}'.";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Addressables", "Failed: " + e.Message, "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawPaletteSwatches(ColorPaletteSO p)
|
||||
{
|
||||
if (p.Colors == null) return;
|
||||
const int swatch = 14;
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
foreach (var c in p.Colors)
|
||||
{
|
||||
var r = GUILayoutUtility.GetRect(swatch, swatch, GUILayout.Width(swatch), GUILayout.Height(swatch));
|
||||
EditorGUI.DrawRect(r, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreatePalette()
|
||||
{
|
||||
EnsureFolder(DefaultPaletteFolder);
|
||||
var path = EditorUtility.SaveFilePanelInProject(
|
||||
"New Color Palette", "NewPalette", "asset",
|
||||
"Choose where to save the palette asset.",
|
||||
DefaultPaletteFolder);
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
var p = CreateInstance<ColorPaletteSO>();
|
||||
var so = new SerializedObject(p);
|
||||
FindAutoProp(so, "Id").stringValue = Path.GetFileNameWithoutExtension(path);
|
||||
so.ApplyModifiedProperties();
|
||||
AssetDatabase.CreateAsset(p, path);
|
||||
AssetDatabase.SaveAssets();
|
||||
Refresh();
|
||||
_selectedPalette = p;
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||||
private static void DrawSeparator()
|
||||
{
|
||||
var r = GUILayoutUtility.GetRect(2, 0, GUILayout.Width(2), GUILayout.ExpandHeight(true));
|
||||
EditorGUI.DrawRect(r, new Color(0, 0, 0, 0.25f));
|
||||
}
|
||||
|
||||
private static void EnsureFolder(string assetPath)
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(assetPath)) return;
|
||||
var parts = assetPath.Split('/');
|
||||
var current = parts[0];
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
var next = current + "/" + parts[i];
|
||||
if (!AssetDatabase.IsValidFolder(next))
|
||||
AssetDatabase.CreateFolder(current, parts[i]);
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
private static string SanitizeId(string raw)
|
||||
{
|
||||
if (string.IsNullOrEmpty(raw)) return "shape";
|
||||
var chars = raw.Select(c => char.IsLetterOrDigit(c) || c == '_' ? c : '_').ToArray();
|
||||
var s = new string(chars).Trim('_');
|
||||
return string.IsNullOrEmpty(s) ? "shape" : s;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 64a89f3acbab9486288ef3ff452a39c7
|
||||
@@ -0,0 +1,26 @@
|
||||
using Darkmatter.Core.Data.Static.Features.DrawingTemplate;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Features.DrawingTemplates.Editor
|
||||
{
|
||||
[CustomEditor(typeof(DrawingTemplateSO))]
|
||||
public sealed class DrawingTemplateInspector : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
var t = (DrawingTemplateSO)target;
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("Open in Drawing Editor", GUILayout.Height(28)))
|
||||
DrawingTemplateEditorWindow.OpenAndSelect(t);
|
||||
if (GUILayout.Button("Ping", GUILayout.Width(60), GUILayout.Height(28)))
|
||||
EditorGUIUtility.PingObject(t);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
DrawDefaultInspector();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ac1d35307f6a427082cca127e61ab71
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "Features.DrawingTemplates.Editor",
|
||||
"rootNamespace": "Darkmatter.Features.DrawingTemplates.Editor",
|
||||
"references": [
|
||||
"GUID:6a005d98aed1c4439bc4689802fa2e3b",
|
||||
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
|
||||
"GUID:4cede189a43c349069c614e305683720",
|
||||
"GUID:2ca8c3a66565544118d3d52d3922933b",
|
||||
"GUID:9e24947de15b9834991c9d8411ea37cf",
|
||||
"GUID:69448af7b92c7f342b298e06a37122aa"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32c91add8e49f4e1fb2121cac770862f
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -5,8 +5,11 @@ using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Features.Coloring;
|
||||
using Darkmatter.Core.Contracts.Features.DrawingCatalog;
|
||||
using Darkmatter.Core.Contracts.Features.GameplayFlow;
|
||||
using Darkmatter.Core.Contracts.Features.Loading;
|
||||
using Darkmatter.Core.Contracts.Features.Progression;
|
||||
using Darkmatter.Core.Contracts.Features.ShapeBuilder;
|
||||
using Darkmatter.Core.Contracts.Services.Capture;
|
||||
using Darkmatter.Core.Contracts.Services.Gallery;
|
||||
using Darkmatter.Core.Contracts.Services.Scenes;
|
||||
using Darkmatter.Core.Data.Dynamic.Features.Progression;
|
||||
using Darkmatter.Core.Data.Signals.Features.Coloring;
|
||||
@@ -28,6 +31,9 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
private readonly IShapeBuilderController _shapeBuilder;
|
||||
private readonly IColoringController _coloring;
|
||||
private readonly ISceneService _scenes;
|
||||
private readonly ICaptureService _captureService;
|
||||
private readonly IGameplaySceneRefs _refs;
|
||||
private readonly ILoadingScreen _loadingScreen;
|
||||
private readonly IEventBus _bus;
|
||||
|
||||
private IDrawingTemplate _template;
|
||||
@@ -45,6 +51,9 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
IShapeBuilderController shapeBuilder,
|
||||
IColoringController coloring,
|
||||
ISceneService scenes,
|
||||
ICaptureService captureService,
|
||||
IGameplaySceneRefs refs,
|
||||
ILoadingScreen loadingScreen,
|
||||
IEventBus bus)
|
||||
{
|
||||
_progression = progression;
|
||||
@@ -52,6 +61,9 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
_shapeBuilder = shapeBuilder;
|
||||
_coloring = coloring;
|
||||
_scenes = scenes;
|
||||
_captureService = captureService;
|
||||
_loadingScreen = loadingScreen;
|
||||
_refs = refs;
|
||||
_bus = bus;
|
||||
}
|
||||
|
||||
@@ -62,7 +74,8 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
|
||||
_templateId = _progression.LastOpenedTemplateId;
|
||||
if (string.IsNullOrEmpty(_templateId))
|
||||
throw new Exception("[GameplayFlow] No LastOpenedTemplateId set. ColorbookFlow must call _progression.SetLastOpenedAsync(id) before loading Gameplay.");
|
||||
throw new Exception(
|
||||
"[GameplayFlow] No LastOpenedTemplateId set. ColorbookFlow must call _progression.SetLastOpenedAsync(id) before loading Gameplay.");
|
||||
|
||||
_template = await _catalog.LoadAsync(_templateId);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
@@ -73,41 +86,41 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
await _shapeBuilder.InitializeAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
_assembledSub = _bus.Subscribe<ShapeAssembledSignal>(OnShapeAssembled);
|
||||
_assembledSub = _bus.Subscribe<ShapeAssembledSignal>(OnShapeAssembled);
|
||||
_colorAppliedSub = _bus.Subscribe<ColorAppliedSignal>(OnColorApplied);
|
||||
|
||||
_loadingScreen.SetProgress(1f);
|
||||
if (_phase == DrawingPhase.Coloring)
|
||||
{
|
||||
// Resume direct into coloring: pre-snap every piece. ShapeBuilder
|
||||
// emits ShapeAssembledSignal once counter hits expected; that path
|
||||
// also triggers InitializeColoring with savedColors.
|
||||
var allIds = CollectAllPieceIds(_template);
|
||||
await _shapeBuilder.BuildAsync(_template, allIds, ct);
|
||||
var savedColors = ToColorDict(progress?.regionColors);
|
||||
await _coloring.InitializeRegionsAsync(_template, savedColors, ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _shapeBuilder.BuildAsync(_template, progress?.snappedPieces, ct);
|
||||
}
|
||||
|
||||
_loadingScreen.Hide();
|
||||
}
|
||||
|
||||
public async UniTask BackAsync()
|
||||
public async UniTask BackAsync(CancellationToken ct)
|
||||
{
|
||||
await SaveCurrentAsync(captureThumbnail: _phase == DrawingPhase.Coloring);
|
||||
await SaveCurrentAsync(captureThumbnail: true, CancellationToken.None);
|
||||
_shapeBuilder.Clear();
|
||||
_coloring.Clear();
|
||||
await _scenes.LoadSceneAsync(GameScene.Colorbook, progress: null, cancellationToken: default);
|
||||
await _scenes.LoadSceneAsync(GameScene.Colorbook, progress: null, cancellationToken: ct);
|
||||
}
|
||||
|
||||
public async UniTask SaveAsync()
|
||||
public async UniTask SaveAsync(CancellationToken ct)
|
||||
{
|
||||
// Explicit user-pressed save — capture thumbnail + (planned) native gallery export.
|
||||
await SaveCurrentAsync(captureThumbnail: true);
|
||||
await SaveCurrentAsync(captureThumbnail: true, ct);
|
||||
// TODO: route through ICaptureService + IGalleryService once those exist.
|
||||
}
|
||||
|
||||
public async UniTask NextAsync()
|
||||
public async UniTask NextAsync(CancellationToken ct)
|
||||
{
|
||||
await SaveCurrentAsync(captureThumbnail: true);
|
||||
await SaveCurrentAsync(captureThumbnail: true, ct);
|
||||
_progression.MarkCompleted(_templateId);
|
||||
|
||||
var nextId = _catalog.GetNextTemplate(_templateId);
|
||||
@@ -126,26 +139,25 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
public void OnApplicationPaused()
|
||||
{
|
||||
// Fire-and-forget — pause window is small; PlayerPrefs write is sync under the hood.
|
||||
SaveCurrentAsync(captureThumbnail: false).Forget();
|
||||
SaveCurrentAsync(captureThumbnail: false, CancellationToken.None).Forget();
|
||||
}
|
||||
|
||||
private void OnShapeAssembled(ShapeAssembledSignal signal)
|
||||
{
|
||||
if (signal.TemplateId != _templateId) return;
|
||||
EnterColoringAsync().Forget();
|
||||
EnterColoringAsync(CancellationToken.None).Forget();
|
||||
}
|
||||
|
||||
private async UniTask EnterColoringAsync()
|
||||
private async UniTask EnterColoringAsync(CancellationToken ct)
|
||||
{
|
||||
_phase = DrawingPhase.Coloring;
|
||||
var progress = _progression.GetProgress(_templateId);
|
||||
var savedColors = ToColorDict(progress?.regionColors);
|
||||
|
||||
await _coloring.InitializeRegionsAsync(_template, savedColors, _scopeCts.Token);
|
||||
_shapeBuilder.DespawnDrawing();
|
||||
|
||||
// Bare-assembled snapshot — catalog should show the puzzle outline even if
|
||||
// the child never paints (per readme §12b save matrix).
|
||||
await SaveCurrentAsync(captureThumbnail: true);
|
||||
await SaveCurrentAsync(captureThumbnail: true, ct);
|
||||
}
|
||||
|
||||
private void OnColorApplied(ColorAppliedSignal _)
|
||||
@@ -162,12 +174,15 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
try
|
||||
{
|
||||
await UniTask.Delay(AutosaveDebounceMs, cancellationToken: ct);
|
||||
await SaveCurrentAsync(captureThumbnail: false);
|
||||
await SaveCurrentAsync(captureThumbnail: false, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
/* superseded by next paint or scope end */
|
||||
}
|
||||
catch (OperationCanceledException) { /* superseded by next paint or scope end */ }
|
||||
}
|
||||
|
||||
private async UniTask SaveCurrentAsync(bool captureThumbnail)
|
||||
private async UniTask SaveCurrentAsync(bool captureThumbnail, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_templateId)) return;
|
||||
|
||||
@@ -179,34 +194,26 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
var existing = _progression.GetProgress(_templateId);
|
||||
var progress = new DrawingProgress
|
||||
{
|
||||
templateId = _templateId,
|
||||
phase = _phase,
|
||||
snappedPieces = snappedIds,
|
||||
regionColors = regionEntries,
|
||||
hasThumbnail = captureThumbnail || (existing?.hasThumbnail ?? false),
|
||||
hasBeenCompleted = existing?.hasBeenCompleted ?? false,
|
||||
completionCount = existing?.completionCount ?? 0,
|
||||
UpdatedUtc = DateTime.UtcNow,
|
||||
templateId = _templateId,
|
||||
phase = _phase,
|
||||
snappedPieces = snappedIds,
|
||||
regionColors = regionEntries,
|
||||
hasThumbnail = captureThumbnail || (existing?.hasThumbnail ?? false),
|
||||
hasBeenCompleted = existing?.hasBeenCompleted ?? false,
|
||||
completionCount = existing?.completionCount ?? 0,
|
||||
UpdatedUtc = DateTime.UtcNow,
|
||||
FirstCompletedUtc = existing?.FirstCompletedUtc,
|
||||
};
|
||||
|
||||
byte[] thumbnailPng = null;
|
||||
if (captureThumbnail)
|
||||
{
|
||||
// TODO: replace with ICaptureService.CaptureAsync() once available.
|
||||
// For now we record hasThumbnail=true but write no PNG.
|
||||
thumbnailPng = await _captureService.CapturePngAsync(_refs.PaperRoot.gameObject, 10, ct);
|
||||
}
|
||||
|
||||
await _progression.SaveProgressAsync(progress, thumbnailPng);
|
||||
}
|
||||
|
||||
private static IReadOnlyCollection<string> CollectAllPieceIds(IDrawingTemplate template)
|
||||
{
|
||||
var ids = new List<string>(template.Pieces.Count);
|
||||
foreach (var p in template.Pieces) ids.Add(p.Id);
|
||||
return ids;
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, Color> ToColorDict(List<RegionColorEntry> entries)
|
||||
{
|
||||
var dict = new Dictionary<string, Color>();
|
||||
@@ -227,4 +234,4 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
_scopeCts?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ namespace Darkmatter.Features.History
|
||||
{
|
||||
_view.SetUndoInteractable(_undoStack.CanUndo);
|
||||
_view.SetRedoInteractable(_undoStack.CanRedo);
|
||||
_view.SetClearInteractable(_undoStack.CanUndo || _undoStack.CanRedo);
|
||||
_view.SetClearInteractable(_undoStack.CanUndo);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
@@ -16,20 +15,31 @@ namespace Darkmatter.Features.History
|
||||
|
||||
void Start()
|
||||
{
|
||||
undoButton.onClick.AddListener(() => OnUndoClicked?.Invoke());
|
||||
redoButton.onClick.AddListener(() => OnRedoClicked?.Invoke());
|
||||
clearButton.onClick.AddListener(() => OnClearClicked?.Invoke());
|
||||
if (undoButton != null) undoButton.onClick.AddListener(() => OnUndoClicked?.Invoke());
|
||||
if (redoButton != null) redoButton.onClick.AddListener(() => OnRedoClicked?.Invoke());
|
||||
if (clearButton != null) clearButton.onClick.AddListener(() => OnClearClicked?.Invoke());
|
||||
}
|
||||
|
||||
public void SetUndoInteractable(bool value) => undoButton.interactable = value;
|
||||
public void SetRedoInteractable(bool value) => redoButton.interactable = value;
|
||||
public void SetClearInteractable(bool value) => clearButton.interactable = value;
|
||||
public void SetUndoInteractable(bool value)
|
||||
{
|
||||
if (undoButton != null) undoButton.interactable = value;
|
||||
}
|
||||
|
||||
public void SetRedoInteractable(bool value)
|
||||
{
|
||||
if (redoButton != null) redoButton.interactable = value;
|
||||
}
|
||||
|
||||
public void SetClearInteractable(bool value)
|
||||
{
|
||||
if (clearButton != null) clearButton.interactable = value;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
undoButton.onClick.RemoveListener(() => OnUndoClicked?.Invoke());
|
||||
redoButton.onClick.RemoveListener(() => OnRedoClicked?.Invoke());
|
||||
clearButton.onClick.RemoveListener(() => OnClearClicked?.Invoke());
|
||||
if (undoButton != null) undoButton.onClick.RemoveAllListeners();
|
||||
if (redoButton != null) redoButton.onClick.RemoveAllListeners();
|
||||
if (clearButton != null) clearButton.onClick.RemoveAllListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,20 +11,20 @@ namespace Darkmatter.Features.ShapeBuilder.Commands
|
||||
private readonly Vector2 _prevSize;
|
||||
private readonly Quaternion _prevRot;
|
||||
private readonly Transform _prevParent;
|
||||
private readonly int _prevSiblingIndex;
|
||||
|
||||
public SnapPieceCommand(ShapePiece piece)
|
||||
{
|
||||
_piece = piece;
|
||||
|
||||
var rt = piece.RectTransform;
|
||||
_prevPos = rt.anchoredPosition;
|
||||
_prevSize = rt.sizeDelta;
|
||||
_prevRot = rt.localRotation;
|
||||
_prevParent = rt.parent;
|
||||
_prevParent = piece.HomeParent;
|
||||
_prevSiblingIndex = piece.HomeSiblingIndex;
|
||||
_prevPos = piece.TrayPosition;
|
||||
_prevSize = piece.TraySize;
|
||||
_prevRot = Quaternion.identity;
|
||||
}
|
||||
|
||||
public void Execute() => _piece.SnapInternal();
|
||||
|
||||
public void Undo() => _piece.UnsnapInternal(_prevParent, _prevPos, _prevSize, _prevRot);
|
||||
public void Undo() => _piece.UnsnapInternal(_prevParent, _prevSiblingIndex, _prevPos, _prevSize, _prevRot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
|
||||
{
|
||||
public class ShapeBuilderController : IShapeBuilderController, IDisposable
|
||||
{
|
||||
private const string PiecePrefabKey = "ShapeBuilder/Piece";
|
||||
private const string PiecePrefabKey = "ShapePiece";
|
||||
|
||||
private string _currentTemplateId;
|
||||
private int _expected;
|
||||
@@ -173,6 +173,16 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
|
||||
return _snappedPieceIds;
|
||||
}
|
||||
|
||||
public void DespawnDrawing()
|
||||
{
|
||||
foreach (var piece in _pieces)
|
||||
if (piece != null) Object.Destroy(piece.gameObject);
|
||||
_pieces.Clear();
|
||||
|
||||
if (_drawingInstance != null) Object.Destroy(_drawingInstance);
|
||||
_drawingInstance = null;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_undo.Drop();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Darkmatter.Core.Contracts.Features.GameplayFlow;
|
||||
using Darkmatter.Core.Contracts.Features.History;
|
||||
using Darkmatter.Core.Contracts.Services.Audio;
|
||||
using Darkmatter.Core.Data.Static.Features.ShapeBuilder;
|
||||
@@ -14,19 +15,22 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
|
||||
private readonly ISfxPlayer _sfx;
|
||||
private readonly IEventBus _bus;
|
||||
private readonly IUndoStack _undo;
|
||||
private readonly IGameplaySceneRefs _refs;
|
||||
|
||||
public ShapePieceFactory(
|
||||
ShapeHolderView holder,
|
||||
ShapeBuilderConfig cfg,
|
||||
ISfxPlayer sfx,
|
||||
IEventBus bus,
|
||||
IUndoStack undo)
|
||||
IUndoStack undo,
|
||||
IGameplaySceneRefs refs)
|
||||
{
|
||||
_holder = holder;
|
||||
_cfg = cfg;
|
||||
_sfx = sfx;
|
||||
_bus = bus;
|
||||
_undo = undo;
|
||||
_refs = refs;
|
||||
}
|
||||
|
||||
public ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker slot, Vector2 trayPos)
|
||||
@@ -35,7 +39,7 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
|
||||
go.name = $"Piece_{shape.Id}";
|
||||
|
||||
var piece = go.GetComponent<ShapePiece>();
|
||||
piece.Setup(shape, slot, _cfg, _sfx, _bus, _undo, trayPos);
|
||||
piece.Setup(shape, slot, _cfg, _sfx, _bus, _undo, trayPos, _refs.PaperRoot);
|
||||
return piece;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,19 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
private IUndoStack _undo;
|
||||
private Vector2 _trayPos;
|
||||
private Vector2 _traySize;
|
||||
private RectTransform _dragRoot;
|
||||
private Transform _homeParent;
|
||||
private int _homeSiblingIndex;
|
||||
|
||||
// Per-drag state
|
||||
private RectTransform _rt;
|
||||
private RectTransform _parentRect;
|
||||
private Camera _eventCam;
|
||||
private Vector2 _grabOffset;
|
||||
private Vector2 _trayPosInDragRoot;
|
||||
private Vector2 _dragSizeDelta;
|
||||
private Vector3 _dragLocalScale;
|
||||
private Sequence _previewSeq;
|
||||
private bool _locked;
|
||||
private bool _inPreview;
|
||||
|
||||
@@ -40,6 +47,10 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
public string PieceId => _shape != null ? _shape.Id : null;
|
||||
public bool IsLocked => _locked;
|
||||
public RectTransform RectTransform => _rt != null ? _rt : (_rt = (RectTransform)transform);
|
||||
public Transform HomeParent => _homeParent;
|
||||
public int HomeSiblingIndex => _homeSiblingIndex;
|
||||
public Vector2 TrayPosition => _trayPos;
|
||||
public Vector2 TraySize => _traySize;
|
||||
|
||||
public void Setup(
|
||||
ShapeSO shape,
|
||||
@@ -48,7 +59,8 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
ISfxPlayer sfx,
|
||||
IEventBus bus,
|
||||
IUndoStack undo,
|
||||
Vector2 trayPos)
|
||||
Vector2 trayPos,
|
||||
RectTransform dragRoot)
|
||||
{
|
||||
_shape = shape;
|
||||
_slot = slot;
|
||||
@@ -58,6 +70,10 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
_undo = undo;
|
||||
_trayPos = trayPos;
|
||||
_traySize = shape.DefaultSizeDelta;
|
||||
_dragRoot = dragRoot;
|
||||
|
||||
_homeParent = RectTransform.parent;
|
||||
_homeSiblingIndex = RectTransform.GetSiblingIndex();
|
||||
|
||||
image.sprite = shape.Sprite;
|
||||
ApplyTrayPose();
|
||||
@@ -66,8 +82,18 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
public void OnBeginDrag(PointerEventData e)
|
||||
{
|
||||
if (_locked) return;
|
||||
|
||||
if (_dragRoot != null && RectTransform.parent != _dragRoot)
|
||||
{
|
||||
RectTransform.SetParent(_dragRoot, worldPositionStays: true);
|
||||
RectTransform.SetAsLastSibling();
|
||||
}
|
||||
|
||||
_parentRect = (RectTransform)RectTransform.parent;
|
||||
_eventCam = e.pressEventCamera;
|
||||
_trayPosInDragRoot = RectTransform.anchoredPosition;
|
||||
_dragSizeDelta = RectTransform.sizeDelta;
|
||||
_dragLocalScale = RectTransform.localScale;
|
||||
_grabOffset = RectTransform.anchoredPosition - ScreenToLocal(e.position);
|
||||
_inPreview = false;
|
||||
}
|
||||
@@ -77,75 +103,122 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
if (_locked) return;
|
||||
|
||||
var pointerLocal = ScreenToLocal(e.position) + _grabOffset;
|
||||
var slotPos = _slot.RectTransform.anchoredPosition;
|
||||
float dist = Vector2.Distance(pointerLocal, slotPos);
|
||||
bool insidePreview = IsOverSlot(e.position);
|
||||
|
||||
if (dist <= _cfg.PreviewRadius)
|
||||
if (insidePreview && !_inPreview)
|
||||
{
|
||||
if (!_inPreview)
|
||||
{
|
||||
_inPreview = true;
|
||||
_sfx.Play(SfxId.ShapeHover);
|
||||
}
|
||||
ApplyPreviewLerp(pointerLocal, dist);
|
||||
_inPreview = true;
|
||||
_sfx.Play(SfxId.ShapeHover);
|
||||
AnimatePreviewPose(toSlot: true);
|
||||
}
|
||||
else
|
||||
else if (!insidePreview && _inPreview)
|
||||
{
|
||||
_inPreview = false;
|
||||
RectTransform.anchoredPosition = pointerLocal;
|
||||
RectTransform.sizeDelta = _traySize;
|
||||
RectTransform.localRotation = Quaternion.identity;
|
||||
AnimatePreviewPose(toSlot: false);
|
||||
}
|
||||
|
||||
if (!_inPreview)
|
||||
RectTransform.anchoredPosition = pointerLocal;
|
||||
}
|
||||
|
||||
public void OnEndDrag(PointerEventData e)
|
||||
{
|
||||
if (_locked) return;
|
||||
|
||||
float dist = Vector2.Distance(
|
||||
RectTransform.anchoredPosition,
|
||||
_slot.RectTransform.anchoredPosition);
|
||||
|
||||
if (dist <= _cfg.SnapRadius)
|
||||
_undo.Push(new SnapPieceCommand(this));
|
||||
if (IsOverSlot(e.position))
|
||||
_undo.Push(new SnapPieceCommand(this));
|
||||
else
|
||||
ReturnToTray();
|
||||
}
|
||||
|
||||
private void ApplyPreviewLerp(Vector2 pointerLocal, float dist)
|
||||
private bool IsOverSlot(Vector2 screenPos)
|
||||
{
|
||||
float t = Mathf.Clamp01(1f - dist / _cfg.PreviewRadius);
|
||||
if (_cfg.PreviewCurve != null) t = _cfg.PreviewCurve.Evaluate(t);
|
||||
return RectTransformUtility.RectangleContainsScreenPoint(
|
||||
_slot.RectTransform, screenPos, _eventCam);
|
||||
}
|
||||
|
||||
var slot = _slot.RectTransform;
|
||||
RectTransform.anchoredPosition = Vector2.Lerp(pointerLocal, slot.anchoredPosition, t);
|
||||
RectTransform.sizeDelta = Vector2.Lerp(_traySize, slot.sizeDelta, t);
|
||||
RectTransform.localRotation = Quaternion.Slerp(Quaternion.identity, slot.localRotation, t);
|
||||
private void AnimatePreviewPose(bool toSlot)
|
||||
{
|
||||
if (_previewSeq.isAlive) _previewSeq.Stop();
|
||||
|
||||
if (toSlot)
|
||||
{
|
||||
var slot = _slot.RectTransform;
|
||||
_previewSeq = Sequence.Create()
|
||||
.Group(Tween.UIAnchoredPosition(RectTransform, SlotPosInDragSpace(), _cfg.SnapDuration, Ease.OutQuad))
|
||||
.Group(Tween.LocalScale(RectTransform, SlotScaleInDragSpace(), _cfg.SnapDuration, Ease.OutQuad))
|
||||
.Group(Tween.LocalRotation(RectTransform, SlotRotInDragSpace(), _cfg.SnapDuration, Ease.OutQuad))
|
||||
.Group(Tween.UISizeDelta(RectTransform, slot.sizeDelta, _cfg.SnapDuration, Ease.OutQuad));
|
||||
}
|
||||
else
|
||||
{
|
||||
_previewSeq = Sequence.Create()
|
||||
.Group(Tween.LocalScale(RectTransform, _dragLocalScale, _cfg.SnapDuration, Ease.OutQuad))
|
||||
.Group(Tween.LocalRotation(RectTransform, Quaternion.identity, _cfg.SnapDuration, Ease.OutQuad))
|
||||
.Group(Tween.UISizeDelta(RectTransform, _dragSizeDelta, _cfg.SnapDuration, Ease.OutQuad));
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 SlotPosInDragSpace()
|
||||
{
|
||||
Vector3 worldPos = _slot.RectTransform.position;
|
||||
Vector3 local = _parentRect.InverseTransformPoint(worldPos);
|
||||
Vector2 parentSize = _parentRect.rect.size;
|
||||
Vector2 anchorRef = (RectTransform.anchorMin - _parentRect.pivot) * parentSize;
|
||||
return new Vector2(local.x - anchorRef.x, local.y - anchorRef.y);
|
||||
}
|
||||
|
||||
private void StopPreviewTweens()
|
||||
{
|
||||
if (_previewSeq.isAlive) _previewSeq.Stop();
|
||||
_previewSeq = default;
|
||||
_inPreview = false;
|
||||
}
|
||||
|
||||
private Quaternion SlotRotInDragSpace()
|
||||
{
|
||||
return Quaternion.Inverse(_parentRect.rotation) * _slot.RectTransform.rotation;
|
||||
}
|
||||
|
||||
private Vector3 SlotScaleInDragSpace()
|
||||
{
|
||||
Vector3 parentLossy = _parentRect.lossyScale;
|
||||
Vector3 slotLossy = _slot.RectTransform.lossyScale;
|
||||
return new Vector3(
|
||||
slotLossy.x / Mathf.Max(0.0001f, parentLossy.x),
|
||||
slotLossy.y / Mathf.Max(0.0001f, parentLossy.y),
|
||||
slotLossy.z / Mathf.Max(0.0001f, parentLossy.z));
|
||||
}
|
||||
|
||||
internal void SnapInternal()
|
||||
{
|
||||
StopPreviewTweens();
|
||||
Lock();
|
||||
var slot = _slot.RectTransform;
|
||||
|
||||
Tween.UIAnchoredPosition(RectTransform, slot.anchoredPosition, _cfg.SnapDuration, Ease.OutBack);
|
||||
Tween.UISizeDelta (RectTransform, slot.sizeDelta, _cfg.SnapDuration, Ease.OutBack);
|
||||
Tween.LocalRotation (RectTransform, slot.localRotation, _cfg.SnapDuration, Ease.OutBack);
|
||||
Tween.Position(RectTransform, slot.position, _cfg.SnapDuration, Ease.OutBack);
|
||||
Tween.Rotation(RectTransform, slot.rotation, _cfg.SnapDuration, Ease.OutBack);
|
||||
Tween.LocalScale(RectTransform, slot.localScale, _cfg.SnapDuration, Ease.OutBack);
|
||||
Tween.UISizeDelta(RectTransform, slot.sizeDelta, _cfg.SnapDuration, Ease.OutBack);
|
||||
|
||||
_sfx.Play(SfxId.ShapeSnap);
|
||||
_bus.Publish(new PieceSnappedSignal(_shape.Id));
|
||||
}
|
||||
|
||||
internal void UnsnapInternal(Transform parent, Vector2 pos, Vector2 size, Quaternion rot)
|
||||
internal void UnsnapInternal(Transform parent, int siblingIndex, Vector2 pos, Vector2 size, Quaternion rot)
|
||||
{
|
||||
_locked = false;
|
||||
image.raycastTarget = true;
|
||||
|
||||
RectTransform.SetParent(parent, worldPositionStays: false);
|
||||
Tween.StopAll(onTarget: RectTransform);
|
||||
|
||||
Tween.UIAnchoredPosition(RectTransform, pos, _cfg.ReturnDuration, Ease.OutQuad);
|
||||
Tween.UISizeDelta (RectTransform, size, _cfg.ReturnDuration, Ease.OutQuad);
|
||||
Tween.LocalRotation (RectTransform, rot, _cfg.ReturnDuration, Ease.OutQuad);
|
||||
RectTransform.SetParent(parent, worldPositionStays: false);
|
||||
if (siblingIndex >= 0) RectTransform.SetSiblingIndex(siblingIndex);
|
||||
|
||||
RectTransform.anchoredPosition = pos;
|
||||
RectTransform.sizeDelta = size;
|
||||
RectTransform.localRotation = rot;
|
||||
RectTransform.localScale = Vector3.one;
|
||||
|
||||
_sfx.Play(SfxId.ShapeReturn);
|
||||
_bus.Publish(new PieceUnsnappedSignal(_shape.Id));
|
||||
@@ -155,25 +228,37 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
{
|
||||
Lock();
|
||||
var slot = _slot.RectTransform;
|
||||
RectTransform.anchoredPosition = slot.anchoredPosition;
|
||||
RectTransform.sizeDelta = slot.sizeDelta;
|
||||
RectTransform.localRotation = slot.localRotation;
|
||||
RectTransform.position = slot.position;
|
||||
RectTransform.rotation = slot.rotation;
|
||||
RectTransform.localScale = slot.localScale;
|
||||
RectTransform.sizeDelta = slot.sizeDelta;
|
||||
}
|
||||
|
||||
private void ReturnToTray()
|
||||
{
|
||||
StopPreviewTweens();
|
||||
_sfx.Play(SfxId.ShapeReturn);
|
||||
Sequence.Create()
|
||||
.Group(Tween.UIAnchoredPosition(RectTransform, _trayPos, _cfg.ReturnDuration, Ease.OutQuad))
|
||||
.Group(Tween.UISizeDelta (RectTransform, _traySize, _cfg.ReturnDuration, Ease.OutQuad))
|
||||
.Group(Tween.LocalRotation (RectTransform, Quaternion.identity, _cfg.ReturnDuration, Ease.OutQuad));
|
||||
.Group(Tween.UIAnchoredPosition(RectTransform, _trayPosInDragRoot, _cfg.ReturnDuration, Ease.OutQuad))
|
||||
.Group(Tween.LocalScale(RectTransform, _dragLocalScale, _cfg.ReturnDuration, Ease.OutQuad))
|
||||
.Group(Tween.LocalRotation(RectTransform, Quaternion.identity, _cfg.ReturnDuration, Ease.OutQuad))
|
||||
.Group(Tween.UISizeDelta(RectTransform, _dragSizeDelta, _cfg.ReturnDuration, Ease.OutQuad))
|
||||
.ChainCallback(RestoreToHomeParent);
|
||||
}
|
||||
|
||||
private void RestoreToHomeParent()
|
||||
{
|
||||
if (this == null || RectTransform == null || _homeParent == null) return;
|
||||
RectTransform.SetParent(_homeParent, worldPositionStays: false);
|
||||
RectTransform.SetSiblingIndex(_homeSiblingIndex);
|
||||
ApplyTrayPose();
|
||||
}
|
||||
|
||||
private void Lock()
|
||||
{
|
||||
_locked = true;
|
||||
image.raycastTarget = false;
|
||||
RectTransform.SetParent(_slot.RectTransform.parent, worldPositionStays: false);
|
||||
RectTransform.SetParent(_slot.RectTransform.parent, worldPositionStays: true);
|
||||
}
|
||||
|
||||
private void ApplyTrayPose()
|
||||
|
||||
Reference in New Issue
Block a user