Skip to content

Commit a2930f5

Browse files
feat: drag-drop sprites from sprite list to composition grid
- Manual drag with floating popup showing sprite image (not generic file icon) - Hit-test drop on composition cells to assign sprite to slot - FrameGroup.GetFlatIndex() and SetSpriteId() for slot-level writes - SpriteViewModel.SlotIndex tracks position in SpriteIndex array - Pixel-perfect drag preview with BitmapInterpolationMode.None
1 parent 5c561cc commit a2930f5

5 files changed

Lines changed: 147 additions & 4 deletions

File tree

src/App/MainWindow.axaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,7 @@
578578
Height="{Binding CompositionPreviewHeight}"
579579
HorizontalAlignment="Center" VerticalAlignment="Center">
580580
<Border Background="{Binding CompositionGridBrush}">
581-
<ItemsControl ItemsSource="{Binding CompositionSprites}">
581+
<ItemsControl x:Name="CompositionGrid" ItemsSource="{Binding CompositionSprites}">
582582
<ItemsControl.ItemsPanel>
583583
<ItemsPanelTemplate><WrapPanel/></ItemsPanelTemplate>
584584
</ItemsControl.ItemsPanel>
@@ -1420,7 +1420,10 @@
14201420
<DataTemplate x:DataType="vm:SpriteViewModel">
14211421
<Border Width="38" Height="46" Margin="1" Background="#11111b"
14221422
CornerRadius="3" BorderBrush="#313244" BorderThickness="1"
1423-
ToolTip.Tip="{Binding SpriteId}">
1423+
ToolTip.Tip="{Binding SpriteId}"
1424+
PointerPressed="OnRightSpritePointerPressed"
1425+
PointerMoved="OnRightSpritePointerMoved"
1426+
PointerReleased="OnRightSpritePointerReleased">
14241427
<Border.ContextMenu>
14251428
<ContextMenu>
14261429
<MenuItem Header="Replace" Command="{Binding $parent[ListBox].((vm:MainWindowViewModel)DataContext).ReplaceSpriteCommand}"/>

src/App/MainWindow.axaml.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public MainWindow()
2121
{
2222
InitializeComponent();
2323
Closing += OnWindowClosing;
24+
2425
Loaded += async (_, _) =>
2526
{
2627
if (DataContext is MainWindowViewModel vm)
@@ -294,6 +295,105 @@ private void OnCompositionSpriteDoubleTapped(object? sender, TappedEventArgs e)
294295
}
295296
}
296297

298+
// ── Sprite drag-drop: drag from sprite list → drop on composition cell ──
299+
300+
private SpriteViewModel? _dragSprite;
301+
private Popup? _dragAdorner;
302+
private bool _spriteDragActive;
303+
304+
private void OnRightSpritePointerPressed(object? sender, PointerPressedEventArgs e)
305+
{
306+
if (sender is not Border border || border.DataContext is not SpriteViewModel svm)
307+
return;
308+
309+
var props = e.GetCurrentPoint(border).Properties;
310+
if (!props.IsLeftButtonPressed) return;
311+
312+
_dragSprite = svm;
313+
_spriteDragActive = false;
314+
e.Pointer.Capture(border);
315+
e.Handled = true;
316+
}
317+
318+
private void OnRightSpritePointerMoved(object? sender, PointerEventArgs e)
319+
{
320+
if (_dragSprite == null) return;
321+
322+
if (!_spriteDragActive)
323+
{
324+
_spriteDragActive = true;
325+
// Create floating adorner showing the sprite image
326+
var img = new Image
327+
{
328+
Source = _dragSprite.Bitmap,
329+
Width = 32,
330+
Height = 32,
331+
Stretch = Stretch.None
332+
};
333+
RenderOptions.SetBitmapInterpolationMode(img, Avalonia.Media.Imaging.BitmapInterpolationMode.None);
334+
_dragAdorner = new Popup
335+
{
336+
IsLightDismissEnabled = false,
337+
Placement = PlacementMode.Pointer,
338+
PlacementTarget = this,
339+
Child = new Border
340+
{
341+
Background = Brushes.Transparent,
342+
Opacity = 0.85,
343+
IsHitTestVisible = false,
344+
Child = img
345+
}
346+
};
347+
((Panel)this.Content!).Children.Add(_dragAdorner);
348+
_dragAdorner.Open();
349+
}
350+
351+
// Reposition by re-opening at pointer
352+
if (_dragAdorner != null)
353+
{
354+
_dragAdorner.Close();
355+
_dragAdorner.Open();
356+
}
357+
}
358+
359+
private void OnRightSpritePointerReleased(object? sender, PointerReleasedEventArgs e)
360+
{
361+
if (_dragSprite == null) return;
362+
363+
var draggedSprite = _dragSprite;
364+
_dragSprite = null;
365+
_spriteDragActive = false;
366+
367+
// Close and remove adorner
368+
if (_dragAdorner != null)
369+
{
370+
_dragAdorner.Close();
371+
if (this.Content is Panel panel)
372+
panel.Children.Remove(_dragAdorner);
373+
_dragAdorner = null;
374+
}
375+
376+
e.Pointer.Capture(null);
377+
378+
// Hit-test: find the composition cell under the pointer
379+
var pos = e.GetPosition(this);
380+
var hit = this.InputHitTest(pos);
381+
var visual = hit as Control;
382+
SpriteViewModel? targetSlot = null;
383+
while (visual != null)
384+
{
385+
if (visual.DataContext is SpriteViewModel svm && svm.SlotIndex >= 0)
386+
{
387+
targetSlot = svm;
388+
break;
389+
}
390+
visual = visual.Parent as Control;
391+
}
392+
393+
if (targetSlot != null && DataContext is MainWindowViewModel vm)
394+
vm.AssignSpriteToSlot(targetSlot, draggedSprite.SpriteId);
395+
}
396+
297397
private void OnClientItemDoubleTapped(object? sender, TappedEventArgs e)
298398
{
299399
if (DataContext is MainWindowViewModel vm)

src/App/ViewModels/MainWindowViewModel.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3355,8 +3355,9 @@ private void BuildCompositionGrid()
33553355
{
33563356
int px = pxStart + patX;
33573357
int py = pyStart + patY;
3358-
uint spriteId = fg.GetSpriteId(fg.Width - 1 - col, fg.Height - 1 - row, layer, px, py, pz, frame);
3359-
var svm = new SpriteViewModel { SpriteId = spriteId };
3358+
int flatIdx = fg.GetFlatIndex(fg.Width - 1 - col, fg.Height - 1 - row, layer, px, py, pz, frame);
3359+
uint spriteId = flatIdx >= 0 ? fg.SpriteIndex[flatIdx] : 0;
3360+
var svm = new SpriteViewModel { SpriteId = spriteId, SlotIndex = flatIdx };
33603361
svm.Bitmap = LoadSpriteBitmap(spriteId);
33613362
CompositionSprites.Add(svm);
33623363
}
@@ -3380,6 +3381,25 @@ private void NavigateRightSpriteToId(uint spriteId)
33803381
SelectedRightSprite = RightSprites.FirstOrDefault(s => s.SpriteId == spriteId);
33813382
}
33823383

3384+
/// <summary>Assign a sprite to a composition slot (used by drag-drop from sprite list).</summary>
3385+
public void AssignSpriteToSlot(SpriteViewModel targetSlot, uint newSpriteId)
3386+
{
3387+
if (targetSlot.SlotIndex < 0) return;
3388+
var thing = _currentCompositionThing;
3389+
if (thing == null || thing.FrameGroups.Length == 0) return;
3390+
3391+
int fgIdx = Math.Clamp(CompositionFrameGroupIndex, 0, Math.Max(0, thing.FrameGroups.Length - 1));
3392+
var fg = thing.FrameGroups[fgIdx];
3393+
3394+
fg.SetSpriteId(targetSlot.SlotIndex, newSpriteId);
3395+
3396+
// Update the cell in-place
3397+
targetSlot.SpriteId = newSpriteId;
3398+
targetSlot.Bitmap = LoadSpriteBitmap(newSpriteId);
3399+
3400+
StatusText = $"Set slot {targetSlot.SlotIndex} → sprite {newSpriteId}";
3401+
}
3402+
33833403
// ══════════════════════════════════════════════════════════════════════
33843404
// ── Reset thing to original state ──
33853405
// ══════════════════════════════════════════════════════════════════════

src/App/ViewModels/SpriteViewModel.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,7 @@ public partial class SpriteViewModel : ObservableObject
1111
{
1212
[ObservableProperty] private uint _spriteId;
1313
[ObservableProperty] private WriteableBitmap? _bitmap;
14+
15+
/// <summary>Flat index into FrameGroup.SpriteIndex[] (set only for composition cells).</summary>
16+
public int SlotIndex { get; set; } = -1;
1417
}

src/OTB/DatThingType.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,23 @@ public uint GetSpriteId(int w, int h, int layer, int patternX, int patternY, int
213213
return index < SpriteIndex.Length ? SpriteIndex[index] : 0;
214214
}
215215

216+
/// <summary>Compute the flat index for a given position (same formula as GetSpriteId).</summary>
217+
public int GetFlatIndex(int w, int h, int layer, int patternX, int patternY, int patternZ, int frame)
218+
{
219+
if (Width == 0 || Height == 0 || Layers == 0 || PatternX == 0 || PatternY == 0 || PatternZ == 0 || Frames == 0)
220+
return -1;
221+
int index = ((((((frame % Frames) * PatternZ + patternZ) * PatternY + patternY) * PatternX + patternX)
222+
* Layers + layer) * Height + h) * Width + w;
223+
return index < SpriteIndex.Length ? index : -1;
224+
}
225+
226+
/// <summary>Set sprite ID at a flat index position.</summary>
227+
public void SetSpriteId(int flatIndex, uint spriteId)
228+
{
229+
if (flatIndex >= 0 && flatIndex < SpriteIndex.Length)
230+
SpriteIndex[flatIndex] = spriteId;
231+
}
232+
216233
/// <summary>Creates a deep copy of this frame group.</summary>
217234
public FrameGroup Clone()
218235
{

0 commit comments

Comments
 (0)