This commit is contained in:
iambossTC 2025-07-16 13:41:43 +08:00
commit 37373ab176
10 changed files with 969 additions and 0 deletions

268
.gitignore vendored Normal file
View File

@ -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

49
Hi.Webapi.csproj Normal file
View File

@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64</Platforms>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<OutputType>Library</OutputType>
<AssemblyName>Hi.Webapi</AssemblyName>
<RootNamespace>$(AssemblyName)</RootNamespace>
<Description>HiNC webapi class library.</Description>
<VersionBuild>0</VersionBuild>
<VersionPrefix>3.1.$(VersionBuild)</VersionPrefix>
<PackageTags>HiAPI</PackageTags>
<Configurations>Debug;Release</Configurations>
<Product>$(AssemblyName)</Product>
<ApplicationIcon>techcoordinate.ico</ApplicationIcon>
<Copyright>Copyright 2025 Tech Coordinate Co., Ltd.</Copyright>
<Company>Tech Coordinate Co., Ltd.</Company>
<Authors>Tech Coordinate Co., Ltd.</Authors>
</PropertyGroup>
<Import Project="..\acc\common_info.props" />
<ItemGroup Condition="'$(Configuration)'=='Debug'">
<ProjectReference Include="..\HiDisp\HiDisp.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='Release'">
<PackageReference Include="HiDisp" Version="3.1.*" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<IsIgnoreObfuscar>true</IsIgnoreObfuscar>
<Optimize>true</Optimize>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<Import Project="..\acc\deploy_local_nupkg.targets" Condition="'$(Configuration)'=='Release'" />
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5" />
</ItemGroup>
</Project>

3
Hi.Webapi.http Normal file
View File

@ -0,0 +1,3 @@
@Hi.Webapi_HostAddress = http://localhost:5190
###

461
Hubs/RenderingHub.cs Normal file
View File

@ -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
{
/// <summary>
/// SignalR Hub 用於處理渲染畫布的實時通信
/// </summary>
public class RenderingHub : Hub
{
private readonly RenderingService _renderingService;
private readonly ILogger<RenderingHub> _logger;
private readonly Dictionary<string, byte[]> _lastFrameCache = new();
private readonly Dictionary<string, DateTime> _lastFrameTime = new();
private readonly Dictionary<string, Mat4d> _sketchViewCache = new();
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}");
// 創建並設置測試顯示對象
_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}");
}
/// <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);
}
}
}

50
Program.cs Normal file
View File

@ -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();
}
}
}

View File

@ -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"
}
}
}
}

View File

@ -0,0 +1,85 @@
using Hi.Disp;
using Hi.Geom;
using Hi.Native;
using System.Collections.Concurrent;
namespace Hi.Webapi.Services
{
/// <summary>
/// 管理 DispEngine 實例和渲染操作的服務
/// </summary>
public class RenderingService : IDisposable
{
private readonly ConcurrentDictionary<string, DispEngine> _engines = new();
private readonly ILogger<RenderingService> _logger;
public RenderingService(ILogger<RenderingService> logger)
{
_logger = logger;
}
/// <summary>
/// 創建或獲取一個 DispEngine 實例
/// </summary>
public DispEngine GetOrCreateEngine(string sessionId)
{
return _engines.GetOrAdd(sessionId, id =>
{
_logger.LogInformation($"創建新的 DispEngineSessionId: {id}");
var engine = new DispEngine();
engine.BackgroundColor = new Vec3d(0.1, 0.1, 0.5);
engine.BackgroundOpacity = 0.1;
return engine;
});
}
/// <summary>
/// 移除指定的 DispEngine
/// </summary>
public bool RemoveEngine(string sessionId)
{
if (_engines.TryRemove(sessionId, out var engine))
{
_logger.LogInformation($"移除 DispEngineSessionId: {sessionId}");
try
{
// 停止渲染
engine.IsVisible = false;
// 釋放資源
engine.Dispose();
}
catch (Exception ex)
{
_logger.LogError(ex, $"清理 DispEngine 時發生錯誤SessionId: {sessionId}");
}
return true;
}
return false;
}
/// <summary>
/// 獲取當前活動的引擎數量
/// </summary>
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();
}
}
}

13
WeatherForecast.cs Normal file
View File

@ -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; }
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
appsettings.json Normal file
View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}