diff --git a/app/lib/features/galaxy/galaxy_screen.dart b/app/lib/features/galaxy/galaxy_screen.dart index 09ecef2..b10a1ea 100644 --- a/app/lib/features/galaxy/galaxy_screen.dart +++ b/app/lib/features/galaxy/galaxy_screen.dart @@ -172,6 +172,27 @@ class _GalaxyScreenState extends State _lastTick = now; // 프레임을 오래 건너뛴 뒤(백그라운드 복귀 등) 한 번에 확 도는 것을 막는다. _clock.advance(dt.clamp(0.0, 1 / 20)); + _follow(); + } + + /// 고른 행성을 화면 가운데에 붙든다(프로토타입 `galaxyGroup.position = -myPos` 추적). + /// 중심 전환이 끝난 뒤에도 궤도는 공전·끌기 관성으로 계속 도므로, 한 번 옮기고 말면 + /// 고른 행성이 화면 밖으로 흘러가고 장면이 밀린 채 남는다(10/5 실기기). 배율은 사용자 + /// 핀치 값을 그대로 두고 평행 이동만 맞춘다. + void _follow() { + final p = _selected; + if (p == null || _viewport.isEmpty || _fly.isAnimating) return; + final at = GalaxyField.positionOf(p, _selectedIndex, _clock.spin, _outerRank); + final s = _view.value.getMaxScaleOnAxis(); + final tx = _viewport.width / 2 - at.dx * s; + final ty = _viewport.height / 2 - at.dy * s; + final m = _view.value; + if ((m.storage[12] - tx).abs() < 0.01 && (m.storage[13] - ty).abs() < 0.01) { + return; + } + _view.value = m.clone() + ..setEntry(0, 3, tx) + ..setEntry(1, 3, ty); } /// 선택 행성을 화면 한가운데로 데려오는 변환. diff --git a/app/test/f2_screens_test.dart b/app/test/f2_screens_test.dart index 50a83bb..a01919b 100644 --- a/app/test/f2_screens_test.dart +++ b/app/test/f2_screens_test.dart @@ -11,6 +11,7 @@ import 'package:fancake/features/galaxy/galaxy_screen.dart'; import 'package:fancake/features/galaxy/galaxy_search_screen.dart'; import 'package:fancake/features/galaxy/widgets/zap_button.dart'; import 'package:fancake/features/planet/planet_data.dart'; +import 'package:fancake/features/planet/planet_detail_screen.dart'; import 'eclipse_stubs.dart'; @@ -33,6 +34,24 @@ Future _pump(WidgetTester tester, Widget screen) async { await tester.pump(const Duration(milliseconds: 950)); // 이벤트 띠 슬라이드 } +/// 두 손가락으로 끝까지 확대하며 오른쪽 아래로 끈다(한 손가락 팬은 없다). +Future _pinch(WidgetTester tester) async { + final origin = tester.getTopLeft(find.byType(InteractiveViewer)); + final a = await tester.startGesture(origin + const Offset(4, 4)); + final b = await tester.startGesture( + origin + const Offset(24, 24), + pointer: 7, + ); + for (var i = 1; i <= 10; i++) { + await b.moveTo(origin + Offset(24.0 + i * 32, 24.0 + i * 32)); + await a.moveTo(origin + Offset(4.0 + i * 20, 4.0 + i * 20)); + await tester.pump(); + } + await a.up(); + await b.up(); + await _settle(tester); +} + /// 회전이 도는 동안에도 안전하게 몇 프레임 넘긴다. Future _settle(WidgetTester tester) async { for (var i = 0; i < 6; i++) { @@ -176,17 +195,16 @@ void main() { tester.view.devicePixelRatio = 2.0; addTearDown(tester.view.reset); await _pump(tester, _galaxy()); - // 가운데 내 행성은 궤도를 돌지 않는다 — 궤도 위 행성을 골라 돌린다. await tester.tap(find.byKey(const ValueKey('newjeans'))); await _settle(tester); final before = tester.getCenter(find.text('RANK')); - await tester.drag(find.byType(InteractiveViewer), const Offset(-200, 0)); - await _settle(tester); + // 확대하면 행성이 커지고, 행성 위에 붙은 카드는 행성 가장자리를 따라 밀려난다. + await _pinch(tester); final after = tester.getCenter(find.text('RANK')); expect( (after - before).distance, greaterThan(20), - reason: '화면 고정 패널이면 맵을 끌어도 제자리에 있다', + reason: '화면 고정 패널이면 확대해도 제자리에 있다', ); }); @@ -277,31 +295,63 @@ void main() { // 리뷰 M2 — 행성이 뷰포트 밖이면 HUD 를 아예 그리지 않는다. 카드만 가장자리에 // clamp 해 두면 어느 행성을 가리키는지 알 수 없고 연결선은 화면 밖에 그려진다. - testWidgets('행성이 화면 밖으로 나가면 HUD 도 사라진다', (tester) async { + // 고른 행성은 카메라가 따라가므로(10/5) 확대·초점 이동으로도 화면 밖으로 밀리지 않는다. + testWidgets('확대해도 고른 행성은 가운데에 남고 HUD 도 남는다', (tester) async { await _pump(tester, _galaxy()); + final mid = tester.getCenter(find.byType(InteractiveViewer)); + await _pinch(tester); expect(find.text('RANK'), findsOneWidget); + final mine = tester.widget(find.byType(InteractiveViewer)); + expect(mine.transformationController!.value.getMaxScaleOnAxis(), + greaterThan(GalaxyMotion.focusZoom)); + final at = tester.getCenter(find.text('RANK')); + expect((at.dx - mid.dx).abs(), lessThan(mid.dx), reason: '카드가 화면 안'); + }); - // 끝까지 확대하며 두 손가락을 오른쪽 아래로 끌어 내 행성을 화면 밖으로 보낸다 - // (한 손가락 팬은 없다 — 확대 제스처의 초점 이동만 맵을 옮긴다). + // 10/5 실기기(Flip3) — 다른 행성을 탭하면 중심 전환 뒤에도 궤도가 돌아 고른 행성이 + // 화면 밖으로 흘러가고 내 행성은 가장자리에 남았다. 명세 「행성 터치시 화면 중심 전환」. + testWidgets('고른 행성은 공전·끌기 회전 중에도 화면 가운데에 머문다', (tester) async { + tester.view.physicalSize = const Size(1200, 2000); + tester.view.devicePixelRatio = 2.0; + addTearDown(tester.view.reset); + await _pump(tester, _galaxy()); final viewer = find.byType(InteractiveViewer); - final origin = tester.getTopLeft(viewer); - final a = await tester.startGesture(origin + const Offset(4, 4)); - final b = await tester.startGesture( - origin + const Offset(24, 24), - pointer: 7, + final mid = tester.getCenter(viewer); + final planet = find.byKey(const ValueKey('newjeans')); + + await tester.tap(planet); + for (var i = 0; i < 25; i++) { + await tester.pump(const Duration(milliseconds: 120)); // 3초 공전 + } + expect((tester.getCenter(planet) - mid).distance, lessThan(8)); + + await tester.fling(viewer, const Offset(-300, 0), 1500); + for (var i = 0; i < 25; i++) { + await tester.pump(const Duration(milliseconds: 120)); + } + expect( + (tester.getCenter(planet) - mid).distance, + lessThan(8), + reason: '관성 회전 뒤에도 고른 행성이 가운데', ); - for (var i = 1; i <= 10; i++) { - await b.moveTo(origin + Offset(24.0 + i * 32, 24.0 + i * 32)); - await a.moveTo(origin + Offset(4.0 + i * 20, 4.0 + i * 20)); - await tester.pump(); + }); + + // 10/5 실기기 — HUD 카드 「>」를 눌러도 행성 상세로 가지 않았다. + // 명세 「HUD 터치시 FPI 상세 페이지 이동」. + testWidgets('HUD 카드의 화살표를 누르면 고른 행성의 상세로 간다', (tester) async { + tester.view.physicalSize = const Size(1200, 2000); + tester.view.devicePixelRatio = 2.0; + addTearDown(tester.view.reset); + await _pump(tester, _galaxy()); + await tester.tap(find.byKey(const ValueKey('newjeans'))); + for (var i = 0; i < 10; i++) { + await tester.pump(const Duration(milliseconds: 120)); } - await a.up(); - await b.up(); + await tester.tap(find.byIcon(Icons.chevron_right)); await _settle(tester); - expect(find.text('RANK'), findsNothing); + expect(find.byType(PlanetDetailScreen), findsOneWidget); }); - // 10/4 실기기 — 가로로 끌면 궤도가 돌지 않고 장면이 통째로 밀려 빈 화면이 됐다. testWidgets('가로로 끌면 장면은 제자리에서 궤도가 돌고, 손을 떼도 관성으로 더 돈다', (tester) async { await _pump(tester, _galaxy()); final viewer = tester.widget(