commit 37373ab1760a1954402af7b8e7faf855e694703a Author: iambossTC Date: Wed Jul 16 13:41:43 2025 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..048c4ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,268 @@ +!*.lib + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +!hasp*.exe +!hasp*.dll +!HiLock.dll +!Aladdin*.dll \ No newline at end of file diff --git a/Hi.Webapi.csproj b/Hi.Webapi.csproj new file mode 100644 index 0000000..a7708ea --- /dev/null +++ b/Hi.Webapi.csproj @@ -0,0 +1,49 @@ + + + + net9.0 + enable + enable + x64 + x64 + true + Library + + Hi.Webapi + $(AssemblyName) + HiNC webapi class library. + 0 + 3.1.$(VersionBuild) + HiAPI + Debug;Release + + $(AssemblyName) + techcoordinate.ico + Copyright 2025 Tech Coordinate Co., Ltd. + Tech Coordinate Co., Ltd. + Tech Coordinate Co., Ltd. + + + + + + + + + + + true + true + true + true + + + DEBUG;TRACE + + + + + + + + diff --git a/Hi.Webapi.http b/Hi.Webapi.http new file mode 100644 index 0000000..39bbc37 --- /dev/null +++ b/Hi.Webapi.http @@ -0,0 +1,3 @@ +@Hi.Webapi_HostAddress = http://localhost:5190 + +### diff --git a/Hubs/RenderingHub.cs b/Hubs/RenderingHub.cs new file mode 100644 index 0000000..3b72416 --- /dev/null +++ b/Hubs/RenderingHub.cs @@ -0,0 +1,461 @@ +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 = new(); + private readonly Dictionary _lastFrameTime = new(); + private readonly Dictionary _sketchViewCache = new(); + + 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}"); + + // 創建並設置測試顯示對象 + _logger.LogInformation($"TestDisplayee set - 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 += (bgra, w, 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); + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..f11d784 --- /dev/null +++ b/Program.cs @@ -0,0 +1,50 @@ + +namespace Hi.Webapi +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + builder.Services.AddAuthorization(); + + // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi + builder.Services.AddOpenApi(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.MapOpenApi(); + } + + app.UseHttpsRedirection(); + + app.UseAuthorization(); + + var summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + app.MapGet("/weatherforecast", (HttpContext httpContext) => + { + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = summaries[Random.Shared.Next(summaries.Length)] + }) + .ToArray(); + return forecast; + }) + .WithName("GetWeatherForecast"); + + app.Run(); + } + } +} diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..279e2c9 --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5190", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7273;http://localhost:5190", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Services/RenderingService.cs b/Services/RenderingService.cs new file mode 100644 index 0000000..9c54e0d --- /dev/null +++ b/Services/RenderingService.cs @@ -0,0 +1,85 @@ +using Hi.Disp; +using Hi.Geom; +using Hi.Native; +using System.Collections.Concurrent; + +namespace Hi.Webapi.Services +{ + /// + /// 管理 DispEngine 實例和渲染操作的服務 + /// + public class RenderingService : IDisposable + { + private readonly ConcurrentDictionary _engines = new(); + private readonly ILogger _logger; + + public RenderingService(ILogger logger) + { + _logger = logger; + } + + /// + /// 創建或獲取一個 DispEngine 實例 + /// + public DispEngine GetOrCreateEngine(string sessionId) + { + return _engines.GetOrAdd(sessionId, id => + { + _logger.LogInformation($"創建新的 DispEngine,SessionId: {id}"); + var engine = new DispEngine(); + engine.BackgroundColor = new Vec3d(0.1, 0.1, 0.5); + engine.BackgroundOpacity = 0.1; + return engine; + }); + } + + /// + /// 移除指定的 DispEngine + /// + public bool RemoveEngine(string sessionId) + { + if (_engines.TryRemove(sessionId, out var engine)) + { + _logger.LogInformation($"移除 DispEngine,SessionId: {sessionId}"); + + try + { + // 停止渲染 + engine.IsVisible = false; + + // 釋放資源 + engine.Dispose(); + } + catch (Exception ex) + { + _logger.LogError(ex, $"清理 DispEngine 時發生錯誤,SessionId: {sessionId}"); + } + + return true; + } + return false; + } + + /// + /// 獲取當前活動的引擎數量 + /// + public int GetActiveEngineCount() => _engines.Count; + + public void Dispose() + { + foreach (var kvp in _engines) + { + try + { + kvp.Value.IsVisible = false; + kvp.Value.Dispose(); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Dispose 時清理引擎錯誤,SessionId: {kvp.Key}"); + } + } + _engines.Clear(); + } + } +} \ No newline at end of file diff --git a/WeatherForecast.cs b/WeatherForecast.cs new file mode 100644 index 0000000..66f73fe --- /dev/null +++ b/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace Hi.Webapi +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}