using Microsoft.AspNetCore.SignalR;
using Hi.Geom;
using Hi.Native;
using System.IO.Compression;
using Hi.Webapi.Services;
namespace Hi.Webapi.Hubs
{
///
/// SignalR Hub for Rendering.
///
public class RenderingHub : Hub
{
public RenderingService RenderingService { get; }
public ILogger Logger { get; }
Dictionary SketchViewCache { get; } = [];
public RenderingHub(RenderingService renderingService, ILogger logger)
{
RenderingService = renderingService;
Logger = logger;
}
///
/// 客戶端連接時初始化渲染引擎
///
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}");
}
///
/// 處理鼠標移動事件
///
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;
}
///
/// 處理鼠標按下事件
///
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;
}
///
/// 處理鼠標釋放事件
///
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;
}
///
/// 處理鼠標滾輪事件
///
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;
}
///
/// 處理窗口大小變化
///
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;
}
///
/// 處理可見性變化
///
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;
}
///
/// 設置視圖
///
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;
}
///
/// 處理鍵盤按下事件
///
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;
}
///
/// 處理鍵盤釋放事件
///
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;
}
///
/// 處理觸摸按下事件
///
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;
}
///
/// 處理觸摸移動事件
///
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;
}
///
/// 處理觸摸釋放事件
///
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;
}
///
/// 獲取瀏覽器滾輪縮放比例
///
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 connectionId = Context.ConnectionId;
SketchViewCache.Remove(connectionId);
RenderingService.RemoveEngine(connectionId);
Logger.LogInformation($"Client disconnected: {connectionId}");
await base.OnDisconnectedAsync(exception);
}
}
}