using Microsoft.AspNetCore.SignalR; using Hi.Geom; using Hi.Native; using System.IO.Compression; using Hi.Webapi.Services; namespace Hi.Webapi.Hubs { /// /// SignalR Hub 用於處理渲染畫布的實時通信 /// public class RenderingHub : Hub { private readonly RenderingService _renderingService; private readonly ILogger _logger; private readonly Dictionary _lastFrameCache = []; private readonly Dictionary _lastFrameTime = []; private readonly Dictionary _sketchViewCache = []; public RenderingHub(RenderingService renderingService, ILogger logger) { _renderingService = renderingService; _logger = logger; } /// /// 客戶端連接時初始化渲染引擎 /// public async Task InitializeCanvas(int width, int height) { var sessionId = Context.ConnectionId; _logger.LogInformation($"InitializeCanvas called - SessionId: {sessionId}, Width: {width}, Height: {height}"); var engine = _renderingService.GetOrCreateEngine(sessionId); _logger.LogInformation($"Engine created/retrieved - SessionId: {sessionId}"); // 啟動引擎(必須在設置回調之前) engine.Start(width, height); _logger.LogInformation($"Engine started with size {width}x{height} - SessionId: {sessionId}"); // 在設置回調之前,先捕獲客戶端代理 var clientProxy = Clients.Caller; var logger = _logger; var frameCount = 0; byte[] preImageRgba = null; // 設置渲染回調 unsafe { engine.ImageRequestAfterBufferSwapped += (byte* bgra, int w, int h) => { if (bgra == null) return; // 幀率限制 - 最多每秒30幀 if (_lastFrameTime.TryGetValue(sessionId, out var lastTime)) { var elapsed = DateTime.Now - lastTime; if (elapsed.TotalMilliseconds < 33) // 30 FPS { return; } } _lastFrameTime[sessionId] = DateTime.Now; frameCount++; if (frameCount <= 5) // 只記錄前5幀 { logger.LogInformation($"Frame {frameCount} rendered - Size: {w}x{h}, SessionId: {sessionId}"); } // 檢查寬度和高度是否有效 if (w <= 0 || h <= 0) { logger.LogWarning($"Invalid image size: {w}x{h} - SessionId: {sessionId}"); return; } try { // 在回調中同步處理圖像數據,轉換為 byte[] int wh = w * h; byte[] rgba = new byte[wh * 4]; for (int i = 0; i < wh; i++) { rgba[i * 4] = bgra[i * 4 + 2]; rgba[i * 4 + 1] = bgra[i * 4 + 1]; rgba[i * 4 + 2] = bgra[i * 4]; rgba[i * 4 + 3] = bgra[i * 4 + 3]; } // 檢查圖像是否有變化 if (preImageRgba != null && preImageRgba.SequenceEqual(rgba)) return; preImageRgba = rgba; if (frameCount <= 5) { logger.LogInformation($"Frame {frameCount} sending - Size: {w}x{h}, Data: {rgba.Length} bytes"); } // 壓縮數據 using MemoryStream dstMemoryStream = new MemoryStream(); using GZipStream gZipStream = new GZipStream(dstMemoryStream, CompressionMode.Compress); using MemoryStream srcMemoryStream = new MemoryStream(rgba); srcMemoryStream.CopyTo(gZipStream); gZipStream.Close(); byte[] compressedRgbaArray = dstMemoryStream.ToArray(); if (compressedRgbaArray == null) return; var compressionRatio = (float)compressedRgbaArray.Length / rgba.Length * 100; if (frameCount <= 5) { logger.LogInformation($"Compressed: {compressedRgbaArray.Length} bytes ({compressionRatio:F1}% of original)"); } // 發送壓縮數據到客戶端(注意:第二個參數應該是原始長度,不是壓縮後的長度) _ = clientProxy.SendAsync("ImageUpdate", compressedRgbaArray, rgba.Length, w, h) .ContinueWith(t => { if (t.IsFaulted) { logger.LogError($"Failed to send frame: {t.Exception?.GetBaseException().Message} - SessionId: {sessionId}"); } }); } catch (ObjectDisposedException) { // Hub 已經被釋放,忽略這個錯誤 logger.LogDebug($"Hub已釋放,忽略渲染回調 - SessionId: {sessionId}"); } catch (Exception ex) { logger.LogError(ex, $"渲染回調錯誤 - SessionId: {sessionId}"); } }; } // 設置初始視圖 if (_sketchViewCache.TryGetValue(sessionId, out var cachedView)) { engine.SketchView = cachedView; } else { engine.SetViewToHomeView(); _sketchViewCache[sessionId] = engine.SketchView; } _logger.LogInformation($"View initialized - SessionId: {sessionId}"); // 確保引擎可見並開始渲染 engine.IsVisible = true; _logger.LogInformation($"Engine visibility set to true - SessionId: {sessionId}"); await Clients.Caller.SendAsync("CanvasInitialized", sessionId); _logger.LogInformation($"Canvas initialized for session: {sessionId}"); } /// /// 處理鼠標移動事件 /// public Task HandleMouseMove(double x, double y, int buttonMask) { var sessionId = Context.ConnectionId; _logger.LogDebug($"HandleMouseMove - SessionId: {sessionId}, X: {x}, Y: {y}, ButtonMask: {buttonMask}"); var engine = _renderingService.GetOrCreateEngine(sessionId); var p = new Vec2i((int)x, (int)y); // 移動鼠標 engine.MouseMove(p.x, p.y); // 如果有按鈕按下,處理拖曳 if (buttonMask > 0) { engine.MouseDragTransform(p.x, p.y, new mouse_button_table__transform_view_by_mouse_drag_t() { LEFT_BUTTON = 0, RIGHT_BUTTON = 2 }); _sketchViewCache[sessionId] = engine.SketchView; } return Task.CompletedTask; } /// /// 處理鼠標按下事件 /// public Task HandleMouseDown(double x, double y, int button) { var sessionId = Context.ConnectionId; _logger.LogInformation($"HandleMouseDown - SessionId: {sessionId}, X: {x}, Y: {y}, Button: {button}"); var engine = _renderingService.GetOrCreateEngine(sessionId); engine.MouseButtonDown(button); return Task.CompletedTask; } /// /// 處理鼠標釋放事件 /// public Task HandleMouseUp(double x, double y, int button) { var sessionId = Context.ConnectionId; _logger.LogInformation($"HandleMouseUp - SessionId: {sessionId}, X: {x}, Y: {y}, Button: {button}"); var engine = _renderingService.GetOrCreateEngine(sessionId); engine.MouseButtonUp(button); return Task.CompletedTask; } /// /// 處理鼠標滾輪事件 /// public Task HandleMouseWheel(double x, double y, double deltaX, double deltaY, string browserBrand = "chrome") { var sessionId = Context.ConnectionId; _logger.LogInformation($"HandleMouseWheel - SessionId: {sessionId}, X: {x}, Y: {y}, DeltaX: {deltaX}, DeltaY: {deltaY}, Browser: {browserBrand}"); var engine = _renderingService.GetOrCreateEngine(sessionId); // 根據瀏覽器類型獲取縮放比例 double scale = GetWheelScaling(browserBrand); engine.MouseWheel((int)(deltaX * scale), -(int)(deltaY * scale)); // 使用 MouseWheelTransform 進行視圖變換 engine.MouseWheelTransform( (int)(deltaX * scale * 1000), -(int)(deltaY * scale * 1000), 0.1 / 1000); _sketchViewCache[sessionId] = engine.SketchView; return Task.CompletedTask; } /// /// 處理窗口大小變化 /// public Task HandleResize(int width, int height) { var sessionId = Context.ConnectionId; _logger.LogInformation($"HandleResize - SessionId: {sessionId}, Width: {width}, Height: {height}"); var engine = _renderingService.GetOrCreateEngine(sessionId); // 重置圖像緩存以強制重新渲染 if (_lastFrameCache.ContainsKey(sessionId)) { _lastFrameCache.Remove(sessionId); } engine.Resize(width, height); _logger.LogInformation($"Resize completed - SessionId: {sessionId}"); return Task.CompletedTask; } /// /// 處理可見性變化 /// public Task HandleVisibilityChange(string visibilityState) { var sessionId = Context.ConnectionId; _logger.LogInformation($"HandleVisibilityChange - SessionId: {sessionId}, State: {visibilityState}"); var engine = _renderingService.GetOrCreateEngine(sessionId); engine.IsVisible = visibilityState == "visible"; return Task.CompletedTask; } /// /// 設置視圖 /// public Task SetView(string viewType) { var sessionId = Context.ConnectionId; _logger.LogInformation($"SetView called - SessionId: {sessionId}, ViewType: {viewType}"); var engine = _renderingService.GetOrCreateEngine(sessionId); switch (viewType.ToLower()) { case "front": engine.SetViewToFrontView(); break; case "back": engine.SetViewToFrontView(); engine.TurnBackView(); break; case "left": engine.SetViewToRightView(); engine.TurnBackView(); break; case "right": engine.SetViewToRightView(); break; case "top": engine.SetViewToTopView(); break; case "bottom": engine.SetViewToTopView(); engine.TurnBackView(); break; case "isometric": engine.SetViewToIsometricView(); break; case "home": engine.SetViewToHomeView(); break; default: _logger.LogWarning($"Unknown view type: {viewType} - SessionId: {sessionId}"); break; } _sketchViewCache[sessionId] = engine.SketchView; return Task.CompletedTask; } /// /// 處理鍵盤按下事件 /// public Task HandleKeyDown(string key, string code, bool ctrlKey, bool shiftKey, bool altKey) { var sessionId = Context.ConnectionId; _logger.LogInformation($"HandleKeyDown - SessionId: {sessionId}, Key: {key}, Code: {code}, Ctrl: {ctrlKey}, Shift: {shiftKey}, Alt: {altKey}"); var engine = _renderingService.GetOrCreateEngine(sessionId); // 使用 key 的 HashCode(與 Blazor 版本一致) long keyCode = key.ToLower().GetHashCode(); engine.KeyDown(keyCode); // 使用 KeyDownTransform 進行視圖變換 engine.KeyDownTransform(keyCode, new key_table__transform_view_by_key_pressing_t() { HOME = "home".ToLower().GetHashCode(), PAGE_UP = "pageup".ToLower().GetHashCode(), PAGE_DOWN = "pagedown".ToLower().GetHashCode(), F1 = "f1".ToLower().GetHashCode(), F2 = "f2".ToLower().GetHashCode(), F3 = "f3".ToLower().GetHashCode(), F4 = "f4".ToLower().GetHashCode(), SHIFT = "shift".ToLower().GetHashCode(), ARROW_LEFT = "arrowleft".ToLower().GetHashCode(), ARROW_RIGHT = "arrowright".ToLower().GetHashCode(), ARROW_DOWN = "arrowdown".ToLower().GetHashCode(), ARROW_UP = "arrowup".ToLower().GetHashCode() }); _sketchViewCache[sessionId] = engine.SketchView; return Task.CompletedTask; } /// /// 處理鍵盤釋放事件 /// public Task HandleKeyUp(string key, string code, bool ctrlKey, bool shiftKey, bool altKey) { var sessionId = Context.ConnectionId; _logger.LogInformation($"HandleKeyUp - SessionId: {sessionId}, Key: {key}, Code: {code}, Ctrl: {ctrlKey}, Shift: {shiftKey}, Alt: {altKey}"); var engine = _renderingService.GetOrCreateEngine(sessionId); long keyCode = key.ToLower().GetHashCode(); engine.KeyUp(keyCode); return Task.CompletedTask; } /// /// 處理觸摸按下事件 /// public Task HandleTouchDown(int pointerId, double x, double y) { var sessionId = Context.ConnectionId; _logger.LogInformation($"HandleTouchDown - SessionId: {sessionId}, PointerId: {pointerId}, X: {x}, Y: {y}"); var engine = _renderingService.GetOrCreateEngine(sessionId); engine.TouchDown(pointerId, (int)x, (int)y); return Task.CompletedTask; } /// /// 處理觸摸移動事件 /// public Task HandleTouchMove(int pointerId, double x, double y) { var sessionId = Context.ConnectionId; _logger.LogDebug($"HandleTouchMove - SessionId: {sessionId}, PointerId: {pointerId}, X: {x}, Y: {y}"); var engine = _renderingService.GetOrCreateEngine(sessionId); engine.TouchMove(pointerId, (int)x, (int)y); return Task.CompletedTask; } /// /// 處理觸摸釋放事件 /// public Task HandleTouchUp(int pointerId) { var sessionId = Context.ConnectionId; _logger.LogInformation($"HandleTouchUp - SessionId: {sessionId}, PointerId: {pointerId}"); var engine = _renderingService.GetOrCreateEngine(sessionId); engine.TouchUp(pointerId); return Task.CompletedTask; } /// /// 獲取瀏覽器滾輪縮放比例 /// private double GetWheelScaling(string browserBrand) { switch (browserBrand.ToLower()) { case "firefox": return 1.0; case "chrome": case "edge": case "safari": default: return 0.01; } } /// /// 客戶端斷開連接時清理資源 /// public override async Task OnDisconnectedAsync(Exception exception) { var sessionId = Context.ConnectionId; // 清理緩存 _lastFrameCache.Remove(sessionId); _lastFrameTime.Remove(sessionId); _sketchViewCache.Remove(sessionId); _renderingService.RemoveEngine(sessionId); _logger.LogInformation($"Client disconnected: {sessionId}"); await base.OnDisconnectedAsync(exception); } } }