Lower inventory placement previews in Unity

This commit is contained in:
Meta Vens 2026-08-18 05:26:37 +02:00
commit 3af959bf4f
3 changed files with 129 additions and 5 deletions

View file

@ -17,6 +17,7 @@ namespace GameCult.Eve.UnityUIToolkit
private Vector2 _inventoryDragStart;
private bool _inventoryPointerMoved;
private bool _suppressInventoryClick;
private readonly List<VisualElement> _inventoryPreviewCells = new();
public EveUiToolkitSurfaceLowerer(EveUiToolkitSurfaceOptions? options = null)
{
@ -273,6 +274,7 @@ namespace GameCult.Eve.UnityUIToolkit
return;
if (TryEmitInventoryDrop(document, _inventoryDragSource, component, grid, evt.position, commandSink))
_inventoryDragSource = null;
ClearInventoryPreview();
});
return grid;
}
@ -291,6 +293,7 @@ namespace GameCult.Eve.UnityUIToolkit
_inventoryDragSource = component;
_inventoryDragStart = evt.position;
_inventoryPointerMoved = false;
ClearInventoryPreview();
item.CapturePointer(evt.pointerId);
evt.StopPropagation();
});
@ -300,7 +303,15 @@ namespace GameCult.Eve.UnityUIToolkit
return;
var pointerPosition = new Vector2(evt.position.x, evt.position.y);
if ((pointerPosition - _inventoryDragStart).sqrMagnitude > 16f)
{
_inventoryPointerMoved = true;
var picked = item.panel?.Pick(evt.position);
var targetElement = InventoryGridAncestor(picked);
if (targetElement?.userData is EveSurfaceComponent target)
ShowInventoryPreview(component, target, targetElement, evt.position);
else
ClearInventoryPreview();
}
});
item.RegisterCallback<PointerUpEvent>(evt =>
{
@ -315,6 +326,7 @@ namespace GameCult.Eve.UnityUIToolkit
TryEmitInventoryDrop(document, component, target, targetElement, evt.position, commandSink);
_inventoryDragSource = null;
_suppressInventoryClick = true;
ClearInventoryPreview();
}
evt.StopPropagation();
});
@ -399,6 +411,9 @@ namespace GameCult.Eve.UnityUIToolkit
Math.Max(0f, ParseNumber(target.GetProp("cellGap"), 4f)));
var x = Math.Max(0, (int)Math.Floor(local.x / pitch));
var y = Math.Max(0, (int)Math.Floor(local.y / pitch));
if (!EveInventoryInteraction.TryCreatePlacementPreview(source, target, x, y, out var preview) ||
preview == null || !preview.IsValid)
return false;
if (!EveInventoryInteraction.TryCreateDropRequest(
document, source, target, x, y, "unity-uitoolkit", out var request) || request == null)
return false;
@ -406,6 +421,50 @@ namespace GameCult.Eve.UnityUIToolkit
return true;
}
private void ShowInventoryPreview(
EveSurfaceComponent source,
EveSurfaceComponent target,
VisualElement targetElement,
Vector2 panelPosition)
{
ClearInventoryPreview();
var local = targetElement.WorldToLocal(panelPosition);
var cellSize = Math.Max(1f, ParseNumber(target.GetProp("cellSize"), 72f));
var gap = Math.Max(0f, ParseNumber(target.GetProp("cellGap"), 4f));
var pitch = cellSize + gap;
var x = Math.Max(0, (int)Math.Floor(local.x / Math.Max(1f, pitch)));
var y = Math.Max(0, (int)Math.Floor(local.y / Math.Max(1f, pitch)));
if (!EveInventoryInteraction.TryCreatePlacementPreview(source, target, x, y, out var preview) ||
preview == null)
return;
foreach (var cell in preview.Cells)
{
var element = new VisualElement { pickingMode = PickingMode.Ignore };
element.AddToClassList("eve-inventory-placement-preview");
element.AddToClassList(preview.IsValid
? "eve-inventory-placement-valid"
: "eve-inventory-placement-invalid");
element.style.position = Position.Absolute;
element.style.left = cell.X * pitch;
element.style.top = cell.Y * pitch;
element.style.width = cellSize;
element.style.height = cellSize;
element.style.backgroundColor = preview.IsValid
? new Color(0.2f, 0.9f, 0.65f, 0.34f)
: new Color(1f, 0.25f, 0.2f, 0.38f);
targetElement.Add(element);
_inventoryPreviewCells.Add(element);
}
}
private void ClearInventoryPreview()
{
foreach (var cell in _inventoryPreviewCells)
cell.RemoveFromHierarchy();
_inventoryPreviewCells.Clear();
}
private static int ParseInt(string value, int fallback) =>
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback;

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GameCult.Eve.Surface;
using GameCult.Mesh;
using NUnit.Framework;
@ -244,18 +245,64 @@ namespace GameCult.Eve.UnityUIToolkit.Tests
["targetKind"] = "equipment",
["targetEntityKey"] = "zone.0.entity.1",
["targetIndex"] = "-1",
["dropCommand.cargo"] = "aetheria.daemon.commands.EquipItem"
["dropCommand.cargo"] = "aetheria.daemon.commands.EquipItem",
["payload.shipId"] = "hangar.ship.1",
["payload.expectedHangarRevision"] = "42"
});
Assert.That(EveInventoryInteraction.TryCreateDropRequest(
Document(target), source, target, 7, 9, "unity-test", out var request), Is.True);
Assert.That(request, Is.Not.Null);
Assert.That(request!.Command, Is.EqualTo("aetheria.daemon.commands.EquipItem"));
Assert.That(request.CommandId, Is.Not.Empty);
Assert.That(request.Payload.GetString("originEntityKey"), Is.EqualTo("zone.0.entity.1"));
Assert.That(request.Payload.GetString("originCargoIndex"), Is.EqualTo("2"));
Assert.That(request.Payload.GetString("destinationX"), Is.EqualTo("7"));
Assert.That(request.Payload.GetString("destinationY"), Is.EqualTo("9"));
Assert.That(request.Payload.GetString("hasDestinationPosition"), Is.EqualTo("true"));
Assert.That(request.Payload.GetString("shipId"), Is.EqualTo("hangar.ship.1"));
Assert.That(request.Payload.GetString("expectedHangarRevision"), Is.EqualTo("42"));
}
[Test]
public void InventoryPlacementPreviewUsesIrregularShapeAndCurrentOccupancy()
{
var moving = Component("moving", EveInventoryInteraction.ItemKind, new Dictionary<string, string>
{
["shapeCells"] = "0,0;1,0;0,1"
});
var installed = Component("installed", EveInventoryInteraction.ItemKind, new Dictionary<string, string>
{
["x"] = "2",
["y"] = "1",
["shapeCells"] = "0,0"
});
var grid = new EveSurfaceComponent(
"grid",
EveInventoryInteraction.GridKind,
new Dictionary<string, string>
{
["columns"] = "4",
["rows"] = "3",
["validCells"] = "0,0;1,0;2,0;3,0;0,1;1,1;2,1;3,1;0,2;1,2;2,2;3,2"
},
new[] { installed });
Assert.That(EveInventoryInteraction.TryCreatePlacementPreview(
moving, grid, 0, 0, out var valid), Is.True);
Assert.That(valid!.IsValid, Is.True);
Assert.That(valid.Cells.Select(cell => (cell.X, cell.Y)),
Is.EquivalentTo(new[] { (0, 0), (1, 0), (0, 1) }));
Assert.That(EveInventoryInteraction.TryCreatePlacementPreview(
moving, grid, 1, 1, out var occupied), Is.True);
Assert.That(occupied!.IsValid, Is.False);
Assert.That(occupied.Reason, Is.EqualTo("occupied"));
Assert.That(EveInventoryInteraction.TryCreatePlacementPreview(
moving, grid, 3, 2, out var outside), Is.True);
Assert.That(outside!.IsValid, Is.False);
Assert.That(outside.Reason, Is.EqualTo("outside-grid"));
}
[Test]

View file

@ -14,9 +14,21 @@ foreach ($required in @($UnityExe, $projectPath, $cultLibBuilder)) {
if (-not (Test-Path -LiteralPath $required)) { throw "Required EveUnity UI Toolkit test path not found: $required" }
}
powershell -ExecutionPolicy Bypass -File $cultLibBuilder
& $cultLibBuilder
if ($LASTEXITCODE -ne 0) { throw "CultLib Unity package build failed with exit code $LASTEXITCODE" }
$builtCultLibPackage = Join-Path $CultLibRoot "artifacts\unity\org.gamecult.cultlib"
$stagedDependenciesRoot = Join-Path $repoRoot "artifacts\test-dependencies"
$stagedCultLibPackage = Join-Path $stagedDependenciesRoot "org.gamecult.cultlib"
if (-not (Test-Path -LiteralPath $builtCultLibPackage)) {
throw "CultLib Unity package build did not produce: $builtCultLibPackage"
}
New-Item -ItemType Directory -Force -Path $stagedDependenciesRoot | Out-Null
if (Test-Path -LiteralPath $stagedCultLibPackage) {
[IO.Directory]::Delete($stagedCultLibPackage, $true)
}
Copy-Item -LiteralPath $builtCultLibPackage -Destination $stagedCultLibPackage -Recurse
$runRoot = Join-Path $output (Get-Date -Format "yyyyMMddTHHmmss")
New-Item -ItemType Directory -Force -Path $runRoot | Out-Null
$resultsPath = Join-Path $runRoot "unity-editmode-results.xml"
@ -27,10 +39,16 @@ $arguments = @(
"-assemblyNames", "GameCult.Eve.UnityUIToolkit.Tests",
"-testResults", $resultsPath, "-logFile", $logPath
)
$process = Start-Process -FilePath $UnityExe -ArgumentList $arguments -Wait -PassThru -WindowStyle Hidden
if ($process.ExitCode -ne 0) {
$process = Start-Process -FilePath $UnityExe -ArgumentList $arguments -PassThru -WindowStyle Hidden
if (-not $process.WaitForExit([int][TimeSpan]::FromMinutes(10).TotalMilliseconds)) {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
if (Test-Path -LiteralPath $logPath) { Get-Content -LiteralPath $logPath -Tail 120 }
throw "EveUnity UI Toolkit tests failed with exit code $($process.ExitCode)"
throw "EveUnity UI Toolkit tests timed out after 10 minutes (Unity PID $($process.Id))."
}
$unityExitCode = $process.ExitCode
if ($unityExitCode -ne 0) {
if (Test-Path -LiteralPath $logPath) { Get-Content -LiteralPath $logPath -Tail 120 }
throw "EveUnity UI Toolkit tests failed with exit code $unityExitCode"
}
[xml]$results = Get-Content -LiteralPath $resultsPath -Raw