Hi.Webapi/Hubs/RenderingHub.cs

420 lines
12 KiB
C#
Raw 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 for Rendering.
/// </summary>
public class RenderingHub : Hub
{
public RenderingService RenderingService { get; }
public ILogger<RenderingHub> Logger { get; }
Dictionary<string, Mat4d> SketchViewCache { get; } = [];
public RenderingHub(RenderingService renderingService, ILogger<RenderingHub> logger)
{
RenderingService = renderingService;
Logger = logger;
}
/// <summary>
/// 客戶端連接時初始化渲染引擎
/// </summary>
public async Task InitializeCanvas(int width, int height)
{
var connectionId = Context.ConnectionId;
Logger.LogInformation($"InitializeCanvas called - ConnectionId: {connectionId}, Width: {width}, Height: {height}");
var engine = RenderingService.GetOrCreateEngine(connectionId);
Logger.LogInformation($"Engine created/retrieved - ConnectionId: {connectionId}");
engine.Start(width, height);
Logger.LogInformation($"Engine started with size {width}x{height} - ConnectionId: {connectionId}");
// 捕獲必要的參數,避免閉包問題
var logger = Logger;
var frameCount = 0;
var renderingService = RenderingService;
// 設置渲染回調
unsafe
{
engine.ImageRequestAfterBufferSwapped += (byte* bgra, int w, int h) =>
{
if (bgra == null)
return;
frameCount++;
if (frameCount <= 5) // 只記錄前5幀
logger.LogInformation($"Frame {frameCount} rendered - Size: {w}x{h}, ConnectionId: {connectionId}");
if (w <= 0 || h <= 0)
{
logger.LogWarning($"Invalid image size: {w}x{h} - ConnectionId: {connectionId}");
return;
}
try
{
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 (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)");
// 使用 RenderingService 的線程安全方法發送數據
_ = renderingService.SendImageToClient(
connectionId,
compressedRgbaArray,
rgba.Length,
w,
h)
.ContinueWith(t =>
{
if (t.IsFaulted)
{
logger.LogError($"Failed to send frame: {t.Exception?.GetBaseException().Message} - ConnectionId: {connectionId}");
}
});
}
catch (Exception ex)
{
logger.LogError(ex, $"渲染回調錯誤 - ConnectionId: {connectionId}");
}
};
}
// 設置初始視圖
if (SketchViewCache.TryGetValue(connectionId, out var cachedView))
{
engine.SketchView = cachedView;
}
else
{
engine.SetViewToHomeView();
SketchViewCache[connectionId] = engine.SketchView;
}
Logger.LogInformation($"View initialized - ConnectionId: {connectionId}");
// 確保引擎可見並開始渲染
engine.IsVisible = true;
Logger.LogInformation($"Engine visibility set to true - ConnectionId: {connectionId}");
await Clients.Caller.SendAsync("CanvasInitialized", connectionId);
Logger.LogInformation($"Canvas initialized for session: {connectionId}");
}
/// <summary>
/// 處理鼠標移動事件
/// </summary>
public Task HandleMouseMove(double x, double y, int buttonMask)
{
var connectionId = Context.ConnectionId;
Logger.LogDebug($"HandleMouseMove - ConnectionId: {connectionId}, X: {x}, Y: {y}, ButtonMask: {buttonMask}");
var engine = RenderingService.GetOrCreateEngine(connectionId);
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[connectionId] = engine.SketchView;
}
return Task.CompletedTask;
}
/// <summary>
/// 處理鼠標按下事件
/// </summary>
public Task HandleMouseDown(double x, double y, int button)
{
var connectionId = Context.ConnectionId;
Logger.LogInformation($"HandleMouseDown - ConnectionId: {connectionId}, X: {x}, Y: {y}, Button: {button}");
var engine = RenderingService.GetOrCreateEngine(connectionId);
engine.MouseButtonDown(button);
return Task.CompletedTask;
}
/// <summary>
/// 處理鼠標釋放事件
/// </summary>
public Task HandleMouseUp(double x, double y, int button)
{
var connectionId = Context.ConnectionId;
Logger.LogInformation($"HandleMouseUp - ConnectionId: {connectionId}, X: {x}, Y: {y}, Button: {button}");
var engine = RenderingService.GetOrCreateEngine(connectionId);
engine.MouseButtonUp(button);
return Task.CompletedTask;
}
/// <summary>
/// 處理鼠標滾輪事件
/// </summary>
public Task HandleMouseWheel(double x, double y, double deltaX, double deltaY, string browserBrand = "chrome")
{
var connectionId = Context.ConnectionId;
Logger.LogInformation($"HandleMouseWheel - ConnectionId: {connectionId}, X: {x}, Y: {y}, DeltaX: {deltaX}, DeltaY: {deltaY}, Browser: {browserBrand}");
var engine = RenderingService.GetOrCreateEngine(connectionId);
// 根據瀏覽器類型獲取縮放比例
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[connectionId] = engine.SketchView;
return Task.CompletedTask;
}
/// <summary>
/// 處理窗口大小變化
/// </summary>
public Task HandleResize(int width, int height)
{
var connectionId = Context.ConnectionId;
Logger.LogInformation($"HandleResize - ConnectionId: {connectionId}, Width: {width}, Height: {height}");
var engine = RenderingService.GetOrCreateEngine(connectionId);
//since the resize from js is floating value,
//the resize event may raise with the same integer size.
//the canvas will be blank after resize event.
//ClearCache to trigger the rendering process.
engine.ClearCache();
engine.Resize(width, height);
Logger.LogInformation($"Resize completed - ConnectionId: {connectionId}");
return Task.CompletedTask;
}
/// <summary>
/// 處理可見性變化
/// </summary>
public Task HandleVisibilityChange(string visibilityState)
{
var connectionId = Context.ConnectionId;
Logger.LogInformation($"HandleVisibilityChange - ConnectionId: {connectionId}, State: {visibilityState}");
var engine = RenderingService.GetOrCreateEngine(connectionId);
engine.IsVisible = visibilityState == "visible";
return Task.CompletedTask;
}
/// <summary>
/// 設置視圖
/// </summary>
public Task SetView(string viewType)
{
var connectionId = Context.ConnectionId;
Logger.LogInformation($"SetView called - ConnectionId: {connectionId}, ViewType: {viewType}");
var engine = RenderingService.GetOrCreateEngine(connectionId);
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} - ConnectionId: {connectionId}");
break;
}
SketchViewCache[connectionId] = engine.SketchView;
return Task.CompletedTask;
}
/// <summary>
/// 處理鍵盤按下事件
/// </summary>
public Task HandleKeyDown(string key, string code, bool ctrlKey, bool shiftKey, bool altKey)
{
var connectionId = Context.ConnectionId;
Logger.LogInformation($"HandleKeyDown - ConnectionId: {connectionId}, Key: {key}, Code: {code}, Ctrl: {ctrlKey}, Shift: {shiftKey}, Alt: {altKey}");
var engine = RenderingService.GetOrCreateEngine(connectionId);
// 使用 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[connectionId] = engine.SketchView;
return Task.CompletedTask;
}
/// <summary>
/// 處理鍵盤釋放事件
/// </summary>
public Task HandleKeyUp(string key, string code, bool ctrlKey, bool shiftKey, bool altKey)
{
var connectionId = Context.ConnectionId;
Logger.LogInformation($"HandleKeyUp - ConnectionId: {connectionId}, Key: {key}, Code: {code}, Ctrl: {ctrlKey}, Shift: {shiftKey}, Alt: {altKey}");
var engine = RenderingService.GetOrCreateEngine(connectionId);
long keyCode = key.ToLower().GetHashCode();
engine.KeyUp(keyCode);
return Task.CompletedTask;
}
/// <summary>
/// 處理觸摸按下事件
/// </summary>
public Task HandleTouchDown(int pointerId, double x, double y)
{
var connectionId = Context.ConnectionId;
Logger.LogInformation($"HandleTouchDown - ConnectionId: {connectionId}, PointerId: {pointerId}, X: {x}, Y: {y}");
var engine = RenderingService.GetOrCreateEngine(connectionId);
engine.TouchDown(pointerId, (int)x, (int)y);
return Task.CompletedTask;
}
/// <summary>
/// 處理觸摸移動事件
/// </summary>
public Task HandleTouchMove(int pointerId, double x, double y)
{
var connectionId = Context.ConnectionId;
Logger.LogDebug($"HandleTouchMove - ConnectionId: {connectionId}, PointerId: {pointerId}, X: {x}, Y: {y}");
var engine = RenderingService.GetOrCreateEngine(connectionId);
engine.TouchMove(pointerId, (int)x, (int)y);
return Task.CompletedTask;
}
/// <summary>
/// 處理觸摸釋放事件
/// </summary>
public Task HandleTouchUp(int pointerId)
{
var connectionId = Context.ConnectionId;
Logger.LogInformation($"HandleTouchUp - ConnectionId: {connectionId}, PointerId: {pointerId}");
var engine = RenderingService.GetOrCreateEngine(connectionId);
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 connectionId = Context.ConnectionId;
SketchViewCache.Remove(connectionId);
RenderingService.RemoveEngine(connectionId);
Logger.LogInformation($"Client disconnected: {connectionId}");
await base.OnDisconnectedAsync(exception);
}
}
}