diff --git a/unity-client/Assets/Scripts/UI/ScannerController.cs b/unity-client/Assets/Scripts/UI/ScannerController.cs index 3f6b748..a9b3943 100644 --- a/unity-client/Assets/Scripts/UI/ScannerController.cs +++ b/unity-client/Assets/Scripts/UI/ScannerController.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Runtime.InteropServices; using UnityEngine; using UnityEngine.UI; using UnityEngine.SceneManagement; @@ -11,218 +12,345 @@ using CommanderAILab.Services; namespace CommanderAILab.UI { - /// - /// Scanner scene — live camera feed, capture frame, POST /api/scanner/scan, - /// display result card overlay, add to collection or deck. - /// - public class ScannerController : MonoBehaviour - { - [Header("Navigation")] - [SerializeField] private Button backButton; - - [Header("Camera Feed")] - [SerializeField] private RawImage cameraFeedImage; - [SerializeField] private Button captureButton; - [SerializeField] private Button toggleCameraButton; - [SerializeField] private TMP_Text cameraStatusText; - [SerializeField] private GameObject scanOverlay; // animated scan line - - [Header("Result Panel")] - [SerializeField] private GameObject resultPanel; - [SerializeField] private Image resultCardImage; - [SerializeField] private TMP_Text resultCardName; - [SerializeField] private TMP_Text resultManaCost; - [SerializeField] private TMP_Text resultType; - [SerializeField] private TMP_Text resultSet; - [SerializeField] private TMP_Text resultOracleText; - [SerializeField] private TMP_Text resultConfidence; - [SerializeField] private Button addToCollectionButton; - [SerializeField] private Button addToDeckButton; - [SerializeField] private Button scanAgainButton; - - [Header("Scan History")] - [SerializeField] private Transform historyParent; - [SerializeField] private GameObject historyRowPrefab; - - [Header("Error")] - [SerializeField] private GameObject errorPanel; - [SerializeField] private TMP_Text errorText; - [SerializeField] private Button retryButton; - - private WebCamTexture _webcam; - private int _cameraIndex = 0; - private CardModel _scannedCard; - private bool _isScanning = false; - private readonly List _history = new(); - private readonly List _historyRows = new(); - - private void Start() + /// + /// Scanner scene — live camera feed, capture frame, POST /api/scanner/scan, + /// display result card overlay, add to collection or deck. + /// WebGL fallback: file upload when camera is unavailable. + /// + public class ScannerController : MonoBehaviour { - backButton.onClick.AddListener(OnBack); - captureButton.onClick.AddListener(OnCapture); - toggleCameraButton.onClick.AddListener(ToggleCamera); - addToCollectionButton.onClick.AddListener(OnAddToCollection); - addToDeckButton.onClick.AddListener(OnAddToDeck); - scanAgainButton.onClick.AddListener(OnScanAgain); - retryButton.onClick.AddListener(OnCapture); + [Header("Navigation")] + [SerializeField] private Button backButton; - resultPanel.SetActive(false); - errorPanel.SetActive(false); - scanOverlay.SetActive(false); + [Header("Camera Feed")] + [SerializeField] private RawImage cameraFeedImage; + [SerializeField] private Button captureButton; + [SerializeField] private Button toggleCameraButton; + [SerializeField] private TMP_Text cameraStatusText; + [SerializeField] private GameObject scanOverlay; // animated scan line - StartCamera(_cameraIndex); - } + [Header("WebGL File Upload Fallback")] + [SerializeField] private GameObject fileUploadPanel; + [SerializeField] private Button fileUploadButton; + [SerializeField] private TMP_Text fileUploadStatusText; + [SerializeField] private RawImage uploadPreviewImage; - private void OnDestroy() - { - if (_webcam != null && _webcam.isPlaying) _webcam.Stop(); - } + [Header("Result Panel")] + [SerializeField] private GameObject resultPanel; + [SerializeField] private Image resultCardImage; + [SerializeField] private TMP_Text resultCardName; + [SerializeField] private TMP_Text resultManaCost; + [SerializeField] private TMP_Text resultType; + [SerializeField] private TMP_Text resultSet; + [SerializeField] private TMP_Text resultOracleText; + [SerializeField] private TMP_Text resultConfidence; + [SerializeField] private Button addToCollectionButton; + [SerializeField] private Button addToDeckButton; + [SerializeField] private Button scanAgainButton; - // ── Camera ──────────────────────────────────────────────────────── - private void StartCamera(int idx) - { - if (_webcam != null && _webcam.isPlaying) _webcam.Stop(); + [Header("Scan History")] + [SerializeField] private Transform historyParent; + [SerializeField] private GameObject historyRowPrefab; - var devices = WebCamTexture.devices; - if (devices.Length == 0) - { - cameraStatusText.text = "No camera found"; - captureButton.interactable = false; - return; - } + [Header("Error")] + [SerializeField] private GameObject errorPanel; + [SerializeField] private TMP_Text errorText; + [SerializeField] private Button retryButton; - idx = Mathf.Clamp(idx, 0, devices.Length - 1); - _webcam = new WebCamTexture(devices[idx].name, 1280, 720, 30); - _webcam.Play(); - cameraFeedImage.texture = _webcam; - cameraStatusText.text = $"Camera: {devices[idx].name}"; - captureButton.interactable = true; - } + private WebCamTexture _webcam; + private int _cameraIndex = 0; + private CardModel _scannedCard; + private bool _isScanning = false; + private bool _isWebGL = false; + private readonly List _history = new(); + private readonly List _historyRows = new(); - private void ToggleCamera() - { - var devices = WebCamTexture.devices; - if (devices.Length < 2) return; - _cameraIndex = (_cameraIndex + 1) % devices.Length; - StartCamera(_cameraIndex); - } + // WebGL JS interop for file upload +#if UNITY_WEBGL && !UNITY_EDITOR + [DllImport("__Internal")] + private static extern void WebGLFileUploadInit(string objectName, string methodName); - // ── Capture & Scan ──────────────────────────────────────────────── - private void OnCapture() - { - if (_isScanning || _webcam == null || !_webcam.isPlaying) return; - _isScanning = true; - errorPanel.SetActive(false); - resultPanel.SetActive(false); - scanOverlay.SetActive(true); - captureButton.interactable = false; + [DllImport("__Internal")] + private static extern void WebGLFileUploadClick(); +#else + private static void WebGLFileUploadInit(string objectName, string methodName) { } + private static void WebGLFileUploadClick() { } +#endif - // Grab frame and encode to JPEG - var tex = new Texture2D(_webcam.width, _webcam.height); - tex.SetPixels(_webcam.GetPixels()); - tex.Apply(); - byte[] jpegBytes = tex.EncodeToJPG(85); - Destroy(tex); - - ApiClient.Instance.ScanCard(jpegBytes, - json => + private void Start() { - scanOverlay.SetActive(false); - _isScanning = false; - captureButton.interactable = true; - try - { - var result = JsonConvert.DeserializeObject(json); - if (result?.card != null) ShowResult(result.card, result.confidence); - else ShowError("Card not recognized. Try again."); - } - catch (Exception e) { ShowError($"Parse error: {e.Message}"); } - }, - err => + backButton.onClick.AddListener(OnBack); + captureButton.onClick.AddListener(OnCapture); + toggleCameraButton.onClick.AddListener(ToggleCamera); + addToCollectionButton.onClick.AddListener(OnAddToCollection); + addToDeckButton.onClick.AddListener(OnAddToDeck); + scanAgainButton.onClick.AddListener(OnScanAgain); + retryButton.onClick.AddListener(OnCapture); + if (fileUploadButton != null) + fileUploadButton.onClick.AddListener(OnFileUploadClick); + + resultPanel.SetActive(false); + errorPanel.SetActive(false); + scanOverlay.SetActive(false); + + // Detect WebGL and show appropriate UI +#if UNITY_WEBGL && !UNITY_EDITOR + _isWebGL = true; +#endif + if (_isWebGL) + InitWebGLFallback(); + else + StartCamera(_cameraIndex); + } + + private void OnDestroy() { - scanOverlay.SetActive(false); - _isScanning = false; - captureButton.interactable = true; - ShowError($"Scan failed:\n{err}"); - }); - } + if (_webcam != null && _webcam.isPlaying) _webcam.Stop(); + } - // ── Result ──────────────────────────────────────────────────────── - private void ShowResult(CardModel card, float confidence) - { - _scannedCard = card; - resultPanel.SetActive(true); - - resultCardName.text = card.name; - resultManaCost.text = card.manaCost; - resultType.text = card.typeLine; - resultSet.text = card.set; - resultOracleText.text = card.oracleText; - resultConfidence.text = $"Confidence: {confidence:P0}"; - - if (!string.IsNullOrEmpty(card.imageUrl)) - ImageCache.Instance.GetSprite(card.imageUrl, s => + // ── WebGL File Upload Fallback ────────────────────────────── + private void InitWebGLFallback() { - if (resultCardImage != null) resultCardImage.sprite = s; - }); + // Hide camera UI, show file upload panel + cameraFeedImage.gameObject.SetActive(false); + toggleCameraButton.gameObject.SetActive(false); + captureButton.gameObject.SetActive(false); + if (fileUploadPanel != null) fileUploadPanel.SetActive(true); + if (fileUploadStatusText != null) + fileUploadStatusText.text = "Camera not available in browser.\nUpload a card image to scan."; + cameraStatusText.text = "WebGL Mode: File Upload"; - AddToHistory(card); - } + // Initialize JS file input + WebGLFileUploadInit(gameObject.name, "OnWebGLFileUploaded"); + } - private void AddToHistory(CardModel card) - { - _history.Add(card); - var row = Instantiate(historyRowPrefab, historyParent); - _historyRows.Add(row); - var lbl = row.GetComponentInChildren(); - if (lbl) lbl.text = card.name; - var btn = row.GetComponentInChildren