Hi.Webapi/Hubs/RenderingHub.cs
2025-07-28 22:01:05 +08:00

458 lines
18 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.AspNetCore.SignalR;
using Hi.Geom;
using Hi.Native;
using System.IO.Compression;
using Hi.Webapi.Services;
namespace Hi.Webapi.Hubs
{
/// <summary>
/// SignalR Hub 用於處理渲染畫布的實時通信
/// </summary>
public class RenderingHub : Hub
{
private readonly RenderingService _renderingService;
private readonly ILogger<RenderingHub> _logger;
private readonly Dictionary<string, byte[]> _lastFrameCache = [];
private readonly Dictionary<string, DateTime> _lastFrameTime = [];
private readonly Dictionary<string, Mat4d> _sketchViewCache = [];
public RenderingHub(RenderingService renderingService, ILogger<RenderingHub> logger)
{
_renderingService = renderingService;
_logger = logger;
}
/// <summary>
/// 客戶端連接時初始化渲染引擎
/// </summary>
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}");
}
/// <summary>
/// 處理鼠標移動事件
/// </summary>
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;
}
/// <summary>
/// 處理鼠標按下事件
/// </summary>
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;
}
/// <summary>
/// 處理鼠標釋放事件
/// </summary>
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;
}
/// <summary>
/// 處理鼠標滾輪事件
/// </summary>
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;
}
/// <summary>
/// 處理窗口大小變化
/// </summary>
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;
}
/// <summary>
/// 處理可見性變化
/// </summary>
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;
}
/// <summary>
/// 設置視圖
/// </summary>
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;
}
/// <summary>
/// 處理鍵盤按下事件
/// </summary>
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;
}
/// <summary>
/// 處理鍵盤釋放事件
/// </summary>
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;
}
/// <summary>
/// 處理觸摸按下事件
/// </summary>
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;
}
/// <summary>
/// 處理觸摸移動事件
/// </summary>
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;
}
/// <summary>
/// 處理觸摸釋放事件
/// </summary>
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;
}
/// <summary>
/// 獲取瀏覽器滾輪縮放比例
/// </summary>
private double GetWheelScaling(string browserBrand)
{
switch (browserBrand.ToLower())
{
case "firefox":
return 1.0;
case "chrome":
case "edge":
case "safari":
default:
return 0.01;
}
}
/// <summary>
/// 客戶端斷開連接時清理資源
/// </summary>
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);
}
}
}