This commit is contained in:
Savya Bikram Shah
2026-05-29 16:57:24 +05:45
parent 6872158d2d
commit f970210e64
11 changed files with 476 additions and 79 deletions

View File

@@ -328,7 +328,7 @@ namespace Darkmatter.Features.DrawingTemplates.Editor
foreach (var m in markers)
{
if (m.Shape == null) { missing++; continue; }
if (!pieces.Contains(m.Shape)) pieces.Add(m.Shape);
pieces.Add(m.Shape);
}
Undo.RecordObject(t, "Scan Drawing Prefab");
t.EditorSet(t.Id, t.DisplayName, t.DefaultThumbnail, t.DrawingPrefab, t.ColoringPrefab,

View File

@@ -12,6 +12,7 @@ namespace Darkmatter.Features.ShapeBuilder.Commands
private readonly Quaternion _prevRot;
private readonly Transform _prevParent;
private readonly int _prevSiblingIndex;
private SlotMarker _snappedSlot;
public SnapPieceCommand(ShapePiece piece)
{
@@ -23,7 +24,12 @@ namespace Darkmatter.Features.ShapeBuilder.Commands
_prevRot = Quaternion.identity;
}
public void Execute() => _piece.SnapInternal();
public void Execute()
{
if (_snappedSlot != null) _piece.ReassignActiveSlot(_snappedSlot);
_piece.SnapInternal();
if (_snappedSlot == null) _snappedSlot = _piece.ActiveSlot;
}
public void Undo() => _piece.UnsnapInternal(_prevParent, _prevSiblingIndex, _prevPos, _prevSize, _prevRot);
}

View File

@@ -6,6 +6,6 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
{
public interface IShapePieceFactory
{
ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker slot, Vector2 trayPos);
ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker[] candidateSlots, Vector2 trayPos);
}
}

View File

@@ -117,28 +117,40 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
int count,
SlotMarker[] slots, float pitch, float trayW)
{
var preSnapCounts = new Dictionary<string, int>();
if (preSnappedIds != null)
foreach (var id in preSnappedIds)
{
if (string.IsNullOrEmpty(id)) continue;
preSnapCounts[id] = preSnapCounts.GetValueOrDefault(id) + 1;
}
for (int i = 0; i < count; i++)
{
var shape = template.Pieces[i];
var slot = FindSlotForShape(slots, shape);
if (slot == null)
var candidates = FindSlotsForShape(slots, shape);
if (candidates.Length == 0)
{
Debug.LogError($"[ShapeBuilder] No SlotMarker for '{shape.Id}' in '{template.Id}'");
continue;
}
var trayPos = new Vector2(pitch * (i + 1) - trayW * 0.5f, 0f);
bool preSnapped = preSnappedIds != null && preSnappedIds.Contains(shape.Id);
var piece = _factory.Create(_piecePrefab, shape, slot, trayPos);
var piece = _factory.Create(_piecePrefab, shape, candidates, trayPos);
_pieces.Add(piece);
if (preSnapped)
if (preSnapCounts.TryGetValue(shape.Id, out var remaining) && remaining > 0)
{
_undo.Append(new SnapPieceCommand(piece));
piece.SnapInstantly();
_snapped++;
_snappedPieceIds.Add(shape.Id);
var unoccupied = FindFirstUnoccupied(candidates);
if (unoccupied != null)
{
_undo.Append(new SnapPieceCommand(piece));
piece.SnapInstantlyTo(unoccupied);
unoccupied.SetOccupied(true);
_snapped++;
_snappedPieceIds.Add(shape.Id);
preSnapCounts[shape.Id] = remaining - 1;
}
}
}
}
@@ -160,10 +172,19 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
}
}
private static SlotMarker FindSlotForShape(SlotMarker[] slots, ShapeSO shape)
private static SlotMarker[] FindSlotsForShape(SlotMarker[] slots, ShapeSO shape)
{
var list = new List<SlotMarker>();
foreach (var s in slots)
if (s != null && s.Shape == shape)
list.Add(s);
return list.ToArray();
}
private static SlotMarker FindFirstUnoccupied(SlotMarker[] slots)
{
foreach (var s in slots)
if (s.Shape == shape)
if (s != null && !s.IsOccupied)
return s;
return null;
}

View File

@@ -33,13 +33,13 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
_refs = refs;
}
public ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker slot, Vector2 trayPos)
public ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker[] candidateSlots, Vector2 trayPos)
{
var go = Object.Instantiate(prefab, _holder.SpawnRoot);
go.name = $"Piece_{shape.Id}";
var piece = go.GetComponent<ShapePiece>();
piece.Setup(shape, slot, _cfg, _sfx, _bus, _undo, trayPos, _refs.PaperRoot);
piece.Setup(shape, candidateSlots, _cfg, _sfx, _bus, _undo, trayPos, _refs.PaperRoot);
return piece;
}
}

View File

@@ -20,7 +20,8 @@ namespace Darkmatter.Features.ShapeBuilder.UI
// Bound by Setup
private ShapeSO _shape;
private SlotMarker _slot;
private SlotMarker[] _candidateSlots;
private SlotMarker _activeSlot;
private ShapeBuilderConfig _cfg;
private ISfxPlayer _sfx;
private IEventBus _bus;
@@ -30,6 +31,9 @@ namespace Darkmatter.Features.ShapeBuilder.UI
private RectTransform _dragRoot;
private Transform _homeParent;
private int _homeSiblingIndex;
private Vector2 _origAnchorMin;
private Vector2 _origAnchorMax;
private Vector2 _origPivot;
// Per-drag state
private RectTransform _rt;
@@ -51,10 +55,13 @@ namespace Darkmatter.Features.ShapeBuilder.UI
public int HomeSiblingIndex => _homeSiblingIndex;
public Vector2 TrayPosition => _trayPos;
public Vector2 TraySize => _traySize;
public SlotMarker ActiveSlot => _activeSlot;
public void ReassignActiveSlot(SlotMarker slot) => _activeSlot = slot;
public void Setup(
ShapeSO shape,
SlotMarker slot,
SlotMarker[] candidateSlots,
ShapeBuilderConfig cfg,
ISfxPlayer sfx,
IEventBus bus,
@@ -62,9 +69,10 @@ namespace Darkmatter.Features.ShapeBuilder.UI
Vector2 trayPos,
RectTransform dragRoot)
{
_shape = shape;
_slot = slot;
_cfg = cfg;
_shape = shape;
_candidateSlots = candidateSlots;
_activeSlot = null;
_cfg = cfg;
_sfx = sfx;
_bus = bus;
_undo = undo;
@@ -74,6 +82,9 @@ namespace Darkmatter.Features.ShapeBuilder.UI
_homeParent = RectTransform.parent;
_homeSiblingIndex = RectTransform.GetSiblingIndex();
_origAnchorMin = RectTransform.anchorMin;
_origAnchorMax = RectTransform.anchorMax;
_origPivot = RectTransform.pivot;
image.sprite = shape.Sprite;
ApplyTrayPose();
@@ -103,17 +114,25 @@ namespace Darkmatter.Features.ShapeBuilder.UI
if (_locked) return;
var pointerLocal = ScreenToLocal(e.position) + _grabOffset;
bool insidePreview = IsOverSlot(e.position);
var hovered = FindSlotUnder(e.position);
bool insidePreview = hovered != null;
if (insidePreview && !_inPreview)
{
_activeSlot = hovered;
_inPreview = true;
_sfx.Play(SfxId.ShapeHover);
AnimatePreviewPose(toSlot: true);
}
else if (insidePreview && _inPreview && hovered != _activeSlot)
{
_activeSlot = hovered;
AnimatePreviewPose(toSlot: true);
}
else if (!insidePreview && _inPreview)
{
_inPreview = false;
_activeSlot = null;
AnimatePreviewPose(toSlot: false);
}
@@ -125,25 +144,39 @@ namespace Darkmatter.Features.ShapeBuilder.UI
{
if (_locked) return;
if (IsOverSlot(e.position))
var target = FindSlotUnder(e.position);
if (target != null)
{
_activeSlot = target;
_undo.Push(new SnapPieceCommand(this));
}
else
{
_activeSlot = null;
ReturnToTray();
}
}
private bool IsOverSlot(Vector2 screenPos)
private SlotMarker FindSlotUnder(Vector2 screenPos)
{
return RectTransformUtility.RectangleContainsScreenPoint(
_slot.RectTransform, screenPos, _eventCam);
if (_candidateSlots == null) return null;
foreach (var s in _candidateSlots)
{
if (s == null) continue;
if (s.IsOccupied && s != _activeSlot) continue;
if (RectTransformUtility.RectangleContainsScreenPoint(s.RectTransform, screenPos, _eventCam))
return s;
}
return null;
}
private void AnimatePreviewPose(bool toSlot)
{
if (_previewSeq.isAlive) _previewSeq.Stop();
if (toSlot)
if (toSlot && _activeSlot != null)
{
var slot = _slot.RectTransform;
var slot = _activeSlot.RectTransform;
_previewSeq = Sequence.Create()
.Group(Tween.UIAnchoredPosition(RectTransform, SlotPosInDragSpace(), _cfg.SnapDuration, Ease.OutQuad))
.Group(Tween.LocalScale(RectTransform, SlotScaleInDragSpace(), _cfg.SnapDuration, Ease.OutQuad))
@@ -161,7 +194,7 @@ namespace Darkmatter.Features.ShapeBuilder.UI
private Vector2 SlotPosInDragSpace()
{
Vector3 worldPos = _slot.RectTransform.position;
Vector3 worldPos = _activeSlot.RectTransform.position;
Vector3 local = _parentRect.InverseTransformPoint(worldPos);
Vector2 parentSize = _parentRect.rect.size;
Vector2 anchorRef = (RectTransform.anchorMin - _parentRect.pivot) * parentSize;
@@ -177,13 +210,13 @@ namespace Darkmatter.Features.ShapeBuilder.UI
private Quaternion SlotRotInDragSpace()
{
return Quaternion.Inverse(_parentRect.rotation) * _slot.RectTransform.rotation;
return Quaternion.Inverse(_parentRect.rotation) * _activeSlot.RectTransform.rotation;
}
private Vector3 SlotScaleInDragSpace()
{
Vector3 parentLossy = _parentRect.lossyScale;
Vector3 slotLossy = _slot.RectTransform.lossyScale;
Vector3 slotLossy = _activeSlot.RectTransform.lossyScale;
return new Vector3(
slotLossy.x / Mathf.Max(0.0001f, parentLossy.x),
slotLossy.y / Mathf.Max(0.0001f, parentLossy.y),
@@ -194,27 +227,49 @@ namespace Darkmatter.Features.ShapeBuilder.UI
{
StopPreviewTweens();
Lock();
var slot = _slot.RectTransform;
var slot = _activeSlot.RectTransform;
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);
Sequence.Create()
.Group(Tween.Position(RectTransform, slot.position, _cfg.SnapDuration, Ease.OutBack))
.Group(Tween.Rotation(RectTransform, slot.rotation, _cfg.SnapDuration, Ease.OutBack))
.Group(Tween.LocalScale(RectTransform, slot.localScale, _cfg.SnapDuration, Ease.OutBack))
.Group(Tween.UISizeDelta(RectTransform, slot.sizeDelta, _cfg.SnapDuration, Ease.OutBack))
.ChainCallback(AlignRectToSlot);
_sfx.Play(SfxId.ShapeSnap);
_bus.Publish(new PieceSnappedSignal(_shape.Id));
}
private void AlignRectToSlot()
{
if (this == null || _activeSlot == null) return;
var rt = RectTransform;
var slot = _activeSlot.RectTransform;
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.pivot = slot.pivot;
rt.anchoredPosition = Vector2.zero;
rt.sizeDelta = Vector2.zero;
rt.localRotation = Quaternion.identity;
rt.localScale = Vector3.one;
}
internal void UnsnapInternal(Transform parent, int siblingIndex, Vector2 pos, Vector2 size, Quaternion rot)
{
_locked = false;
image.raycastTarget = true;
if (_activeSlot != null) _activeSlot.SetOccupied(false);
_activeSlot = null;
Tween.StopAll(onTarget: RectTransform);
RectTransform.SetParent(parent, worldPositionStays: false);
if (siblingIndex >= 0) RectTransform.SetSiblingIndex(siblingIndex);
RectTransform.anchorMin = _origAnchorMin;
RectTransform.anchorMax = _origAnchorMax;
RectTransform.pivot = _origPivot;
RectTransform.anchoredPosition = pos;
RectTransform.sizeDelta = size;
RectTransform.localRotation = rot;
@@ -224,14 +279,11 @@ namespace Darkmatter.Features.ShapeBuilder.UI
_bus.Publish(new PieceUnsnappedSignal(_shape.Id));
}
public void SnapInstantly()
public void SnapInstantlyTo(SlotMarker slot)
{
_activeSlot = slot;
Lock();
var slot = _slot.RectTransform;
RectTransform.position = slot.position;
RectTransform.rotation = slot.rotation;
RectTransform.localScale = slot.localScale;
RectTransform.sizeDelta = slot.sizeDelta;
AlignRectToSlot();
}
private void ReturnToTray()
@@ -258,7 +310,11 @@ namespace Darkmatter.Features.ShapeBuilder.UI
{
_locked = true;
image.raycastTarget = false;
RectTransform.SetParent(_slot.RectTransform.parent, worldPositionStays: true);
if (_activeSlot != null)
{
_activeSlot.SetOccupied(true);
RectTransform.SetParent(_activeSlot.RectTransform, worldPositionStays: true);
}
}
private void ApplyTrayPose()

View File

@@ -12,6 +12,9 @@ namespace Darkmatter.Features.ShapeBuilder.UI
public ShapeSO Shape => shape;
public string SlotId => shape != null ? shape.Id : null;
public RectTransform RectTransform => (RectTransform)transform;
public bool IsOccupied { get; private set; }
public void SetOccupied(bool value) => IsOccupied = value;
public void SetOutlineVisible(bool visible)
{