Derive native drag fixtures from selectable integer pointer input

This commit is contained in:
NINI
2026-10-10 07:49:48 +09:00
parent 72638ab091
commit ff5d7e1b96
@@ -61,7 +61,8 @@ namespace MagnetismHeroes.Tests
{ {
public string name, ballId, kind = "boundary"; public string name, ballId, kind = "boundary";
public FreePlayState source, expected; public FreePlayState source, expected;
public Vector3 requested; public Vector3 requested, grabOffset, pointerDelta;
public string firstHitId;
public Vector2 start, end; public Vector2 start, end;
} }
private Store _store; private Store _store;
@@ -139,6 +140,60 @@ namespace MagnetismHeroes.Tests
} }
[UnityTearDown] public IEnumerator TearDown() { Object.Destroy(_app.gameObject); yield return null; } [UnityTearDown] public IEnumerator TearDown() { Object.Destroy(_app.gameObject); yield return null; }
private Vector2 NativePoint(Vector3 world)
{
var viewport = _app.Orbit.Camera.WorldToViewportPoint(world);
return new Vector2((float)Math.Round(viewport.x * 2400), (float)Math.Round((1 - viewport.y) * 1080));
}
private Vector2 PlayerPoint(Vector2 native) => _app.Orbit.Camera.ViewportToScreenPoint(new Vector3(native.x / 2400, 1 - native.y / 1080));
private NativeDrag ExerciseNativeDrag(string name, string kind, FreePlayState source, Vector3 requested)
{
var id = source.balls[0].id; var origin = BoardPresenter.ToVector(source.balls[0].position);
Assert.That(JsonUtility.ToJson(_app.Session.Camera), Is.EqualTo(JsonUtility.ToJson(source.camera)), "Exported camera must be the actual confirmed manual pose.");
Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(JsonUtility.ToJson(source.camera)));
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Session.Camera)), Is.EqualTo(JsonUtility.ToJson(source)), "Entry must preserve every fixture ID, position and group.");
var start = NativePoint(origin); Physics.SyncTransforms();
var ray = _app.Orbit.Camera.ScreenPointToRay(PlayerPoint(start));
var nativeRay = _app.Orbit.Camera.ViewportPointToRay(new Vector3(start.x / 2400, 1 - start.y / 1080));
Assert.That(Vector3.Distance(ray.origin, nativeRay.origin), Is.LessThan(.00001f));
Assert.That(Vector3.Distance(ray.direction, nativeRay.direction), Is.LessThan(.00001f), "Editor pixelRect must preserve the same native viewport ray.");
Assert.That(Physics.Raycast(ray, out var hit, 100), Is.True);
Assert.That(hit.collider.TryGetComponent<BallView>(out var ball) ? ball.BallId : hit.collider.name, Is.EqualTo(id), name + " actual first hit must select the requested sphere.");
var grabOffset = hit.point - origin;
var end = NativePoint(origin + requested + grabOffset);
foreach (var point in new[] { start, end })
Assert.That(point.x >= 530 && point.x <= 1850 && point.y >= 280 && point.y <= 750, Is.True, name + " integer native pointer must stay in the phone world UI-safe region.");
var plane = new Plane(_app.Orbit.Camera.transform.forward, hit.point);
var endRay = _app.Orbit.Camera.ScreenPointToRay(PlayerPoint(end));
Assert.That(plane.Raycast(ray, out var from), Is.True); Assert.That(plane.Raycast(endRay, out var to), Is.True);
var pointerDelta = endRay.GetPoint(to) - ray.GetPoint(from);
var saved = JsonUtility.ToJson(_store.Saved);
_app.Input.Begin(PlayerPoint(start), false); _app.Input.Move(PlayerPoint(end));
Assert.That(_app.Orbit.IsAssisting, Is.True, "Actual BoardInput must acquire and move this friend.");
Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved), "Preview and assistance cannot save intermediate state.");
var actual = _app.Presenter.DropDelta; var merge = _app.Presenter.MergeCandidate;
if (kind == "boundary") Assert.That(actual.magnitude, Is.InRange(1.19f, 1.21f), "Actual pointer must stop at the .5 sphere margin.");
if (kind == "foliage-pass")
{
Assert.That(Vector3.Distance(actual, pointerDelta), Is.LessThan(.001f), "The entire actual integer-pointer movement through foliage must pass without clipping.");
Assert.That(actual.x, Is.GreaterThan(3.9f));
}
if (kind == "thin-obstacle") { Assert.That(actual.x, Is.GreaterThan(.1f).And.LessThan(requested.x - 1)); Assert.That(merge, Is.Null); }
if (kind == "two-plus-three") Assert.That(merge, Is.EqualTo("native-contact-target"));
var preview = _app.Presenter.Balls.ToDictionary(pair => pair.Key, pair => pair.Value.transform.position);
var members = source.groups.Single(group => group.ballIds.Contains(id)).ballIds;
foreach (var value in source.balls)
Assert.That(Vector3.Distance(preview[value.id], BoardPresenter.ToVector(value.position) + (members.Contains(value.id) ? actual : Vector3.zero)), Is.LessThan(.001f), "Only the whole grabbed group may translate; all relative offsets and other friends are preserved.");
_app.Input.End(PlayerPoint(end));
Assert.That(_app.Session.Board.Balls.Select(value => value.id), Is.EquivalentTo(source.balls.Select(value => value.id)));
foreach (var value in _app.Session.Board.Balls) Assert.That(Vector3.Distance(BoardPresenter.ToVector(value.position), preview[value.id]), Is.LessThan(.001f), "Drop must equal the actual input preview for every ID.");
Assert.That(JsonUtility.ToJson(_app.Session.Camera), Is.EqualTo(JsonUtility.ToJson(source.camera)));
if (kind != "two-plus-three") Assert.That(_app.Session.Board.Groups.Select(group => JsonUtility.ToJson(group)), Is.EqualTo(source.groups.Select(group => JsonUtility.ToJson(group))));
else Assert.That(_app.Session.Board.Groups.Single().ballIds, Is.EquivalentTo(source.balls.Select(value => value.id)));
return new NativeDrag { name = name, kind = kind, ballId = id, source = source, requested = requested, start = start, end = end,
grabOffset = grabOffset, pointerDelta = pointerDelta, firstHitId = ball.BallId, expected = _app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()) };
}
[UnityTest] public IEnumerator ExportActualMainPlaygroundNativeBoundaryFixtures() [UnityTest] public IEnumerator ExportActualMainPlaygroundNativeBoundaryFixtures()
{ {
var stages = new System.Collections.Generic.List<NativeStage>(); var stages = new System.Collections.Generic.List<NativeStage>();
@@ -233,13 +288,8 @@ namespace MagnetismHeroes.Tests
_store.Saved.latestFreePlay = source; _app.Initialize(_store); _app.ContinueFreePlay(); yield return null; _store.Saved.latestFreePlay = source; _app.Initialize(_store); _app.ContinueFreePlay(); yield return null;
_app.Orbit.Camera.aspect = 2400f / 1080; _app.Orbit.Camera.aspect = 2400f / 1080;
var requested = Vector3.zero; requested[axis] = sign * 3; var requested = Vector3.zero; requested[axis] = sign * 3;
var start = _app.Orbit.Camera.WorldToViewportPoint(points[0]); var end = _app.Orbit.Camera.WorldToViewportPoint(points[0] + requested); drags.Add(ExerciseNativeDrag("boundary-" + count + "-" + axis + "-" + sign, "boundary", source, requested));
var actual = _app.Environment.ConstrainTranslation(points, requested, _app.Presenter.transform); yield return null;
Assert.That(actual.magnitude, Is.InRange(1.19f, 1.21f), "Actual boundary clipping must stop at the .5 sphere margin.");
_app.CommitMove("native-drag-group", BoardPresenter.ToPosition(actual)); yield return null;
drags.Add(new NativeDrag { name = "boundary-" + count + "-" + axis + "-" + sign, ballId = id, source = source,
expected = _app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()), requested = requested,
start = new Vector2(start.x * 2400, (1 - start.y) * 1080), end = new Vector2(end.x * 2400, (1 - end.y) * 1080) });
} }
_app.StartStage(1); yield return null; Physics.SyncTransforms(); _app.StartStage(1); yield return null; Physics.SyncTransforms();
var arch = _app.Environment.GetComponentsInChildren<MeshCollider>().First(value => value.enabled && value.GetComponentsInParent<Transform>().Any(parent => parent.name.Contains("OpenStoneArch"))); var arch = _app.Environment.GetComponentsInChildren<MeshCollider>().First(value => value.enabled && value.GetComponentsInParent<Transform>().Any(parent => parent.name.Contains("OpenStoneArch")));
@@ -279,15 +329,22 @@ namespace MagnetismHeroes.Tests
var source = new FreePlayState { sourceStageId = 1, camera = camera, balls = balls.ToArray(), groups = groups.ToArray() }; var source = new FreePlayState { sourceStageId = 1, camera = camera, balls = balls.ToArray(), groups = groups.ToArray() };
_store.Saved.latestFreePlay = source; _app.Initialize(_store); _app.ContinueFreePlay(); yield return null; _store.Saved.latestFreePlay = source; _app.Initialize(_store); _app.ContinueFreePlay(); yield return null;
Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True); _app.Orbit.Camera.aspect = 2400f / 1080; Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True); _app.Orbit.Camera.aspect = 2400f / 1080;
var start = _app.Orbit.Camera.WorldToViewportPoint(sourcePoint); var end = _app.Orbit.Camera.WorldToViewportPoint(sourcePoint + requested); if (kind == "thin-obstacle")
_app.Presenter.PreviewGroup("native-contact-source", requested); {
var actual = _app.Presenter.DropDelta; var merge = _app.Presenter.MergeCandidate; bool visible = false;
if (kind == "foliage-pass") Assert.That(Vector3.Distance(actual, requested), Is.LessThan(.001f)); foreach (float pitch in new[] { 0f, 15f, 30f, 45f, 60f })
if (kind == "thin-obstacle") { Assert.That(actual.x, Is.GreaterThan(.1f).And.LessThan(requested.x - 1)); Assert.That(merge, Is.Null); } {
if (kind == "two-plus-three") Assert.That(merge, Is.EqualTo("native-contact-target")); var rotation = Quaternion.Euler(pitch, 0, 0);
_app.CommitMove("native-contact-source", BoardPresenter.ToPosition(actual), merge); yield return null; camera.rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w); _app.Orbit.Apply(camera);
drags.Add(new NativeDrag { name = kind, kind = kind, ballId = source.balls[0].id, source = source, expected = _app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()), requested = requested, Physics.SyncTransforms(); var ray = _app.Orbit.Camera.ScreenPointToRay(PlayerPoint(NativePoint(sourcePoint)));
start = new Vector2(start.x * 2400, (1 - start.y) * 1080), end = new Vector2(end.x * 2400, (1 - end.y) * 1080) }); if (!Physics.Raycast(ray, out var hit, 100) || !hit.collider.TryGetComponent<BallView>(out var ball) || ball.BallId != source.balls[0].id) continue;
Assert.That(Quaternion.Angle(_app.Orbit.Camera.transform.rotation, rotation), Is.LessThan(.001f), "Chosen pitch must already be a valid manual pose.");
source.camera = _app.Orbit.Snapshot(); _app.Session.CommitCamera(source.camera); visible = true; break;
}
Assert.That(visible, Is.True, "The same thin-arch source must have an actually selectable supported pitch.");
}
Capture(kind + "-native-pointer-source", "Actual Editor-selected first hit and preserved source; integer native pointer oracle, not Android pass", 2400, 1080);
drags.Add(ExerciseNativeDrag(kind, kind, source, requested)); yield return null;
} }
var directory = Environment.GetEnvironmentVariable("MAGNETISM_NATIVE_FIXTURES") ?? Path.GetFullPath("artifacts/verification/PlaygroundInteraction/NativeFixtures"); var directory = Environment.GetEnvironmentVariable("MAGNETISM_NATIVE_FIXTURES") ?? Path.GetFullPath("artifacts/verification/PlaygroundInteraction/NativeFixtures");
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);