tune.
This commit is contained in:
parent
68c8096686
commit
910236028b
@ -1,7 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Hi.Webapi.Services;
|
|
||||||
using Hi.Disp;
|
using Hi.Disp;
|
||||||
using Hi.Geom;
|
using Hi.Geom;
|
||||||
|
using Hi.Webapi.Services;
|
||||||
|
|
||||||
namespace Sample.Controllers
|
namespace Sample.Controllers
|
||||||
{
|
{
|
||||||
|
@ -21,12 +21,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(Configuration)'=='Debug'">
|
<ItemGroup Condition="'$(Configuration)'=='Debug'">
|
||||||
<ProjectReference Include="..\HiLicense\HiLicense.csproj" />
|
|
||||||
<ProjectReference Include="..\HiGeom\HiGeom.csproj" />
|
|
||||||
<ProjectReference Include="..\HiDisp\HiDisp.csproj" />
|
|
||||||
<ProjectReference Include="..\HiCbtr\HiCbtr.csproj" />
|
|
||||||
<ProjectReference Include="..\HiMech\HiMech.csproj" />
|
|
||||||
<ProjectReference Include="..\HiUniNc\HiUniNc.csproj" />
|
|
||||||
<ProjectReference Include="..\HiNc\HiNc.csproj" />
|
<ProjectReference Include="..\HiNc\HiNc.csproj" />
|
||||||
<ProjectReference Include="..\Hi.Webapi\Hi.Webapi.csproj" />
|
<ProjectReference Include="..\Hi.Webapi\Hi.Webapi.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -2,7 +2,7 @@ using Hi.HiNcKits;
|
|||||||
using Hi.Webapi.Hubs;
|
using Hi.Webapi.Hubs;
|
||||||
using Hi.Webapi.Services;
|
using Hi.Webapi.Services;
|
||||||
|
|
||||||
SingleUserApp.AppBegin();
|
LocalApp.AppBegin();
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
@ -44,7 +44,7 @@ var app = builder.Build();
|
|||||||
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
|
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
|
||||||
lifetime.ApplicationStopping.Register(() =>
|
lifetime.ApplicationStopping.Register(() =>
|
||||||
{
|
{
|
||||||
SingleUserApp.AppEnd();
|
LocalApp.AppEnd();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
@ -183,7 +183,8 @@
|
|||||||
|
|
||||||
<div style="padding: 10px; background-color: #fff3cd; color: #856404; border: 1px solid #ffeaa7; border-radius: 4px; margin: 10px;">
|
<div style="padding: 10px; background-color: #fff3cd; color: #856404; border: 1px solid #ffeaa7; border-radius: 4px; margin: 10px;">
|
||||||
<strong>提示:</strong>點擊畫布以啟用鍵盤控制。畫布獲得焦點時邊框會變成綠色。
|
<strong>提示:</strong>點擊畫布以啟用鍵盤控制。畫布獲得焦點時邊框會變成綠色。
|
||||||
<br>鍵盤控制:F1-F4切換視圖,方向鍵旋轉視圖,PageUp/PageDown縮放,Home重置視圖
|
<br><strong>鍵盤控制:</strong>F1-F4切換視圖,方向鍵旋轉視圖,PageUp/PageDown縮放,Home重置視圖
|
||||||
|
<br><strong>觸控手勢:</strong>單指拖動平移,雙指捏合縮放,雙指旋轉,三指上下滑動縮放
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="view-controls">
|
<div class="view-controls">
|
||||||
@ -637,22 +638,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupTouchEvents() {
|
function setupTouchEvents() {
|
||||||
let touchStartX = 0;
|
|
||||||
let touchStartY = 0;
|
|
||||||
|
|
||||||
canvas.addEventListener('touchstart', async (e) => {
|
canvas.addEventListener('touchstart', async (e) => {
|
||||||
if (!canvasInitialized) return;
|
if (!canvasInitialized) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const touch = e.touches[0];
|
// 處理所有觸控點,支援多點觸控
|
||||||
const rect = canvas.getBoundingClientRect();
|
for (let i = 0; i < e.changedTouches.length; i++) {
|
||||||
touchStartX = touch.clientX - rect.left;
|
const touch = e.changedTouches[i];
|
||||||
touchStartY = touch.clientY - rect.top;
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = touch.clientX - rect.left;
|
||||||
|
const y = touch.clientY - rect.top;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connection.invoke("HandleTouchDown", touch.identifier, touchStartX, touchStartY);
|
await connection.invoke("HandleTouchDown", touch.identifier, x, y);
|
||||||
} catch (err) {
|
console.log(`觸控開始 [${touch.identifier}]: (${x}, ${y})`);
|
||||||
console.error("觸摸開始處理失敗:", err);
|
} catch (err) {
|
||||||
|
console.error("觸摸開始處理失敗:", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -660,15 +662,23 @@
|
|||||||
if (!canvasInitialized) return;
|
if (!canvasInitialized) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const touch = e.touches[0];
|
// 處理所有移動的觸控點,支援捏合縮放和旋轉手勢
|
||||||
const rect = canvas.getBoundingClientRect();
|
for (let i = 0; i < e.changedTouches.length; i++) {
|
||||||
const x = touch.clientX - rect.left;
|
const touch = e.changedTouches[i];
|
||||||
const y = touch.clientY - rect.top;
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = touch.clientX - rect.left;
|
||||||
|
const y = touch.clientY - rect.top;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connection.invoke("HandleTouchMove", touch.identifier, x, y);
|
await connection.invoke("HandleTouchMove", touch.identifier, x, y);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("觸摸移動處理失敗:", err);
|
console.error("觸摸移動處理失敗:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 顯示當前觸控點數量(用於調試)
|
||||||
|
if (e.touches.length >= 2) {
|
||||||
|
console.log(`多點觸控中 - 觸控點數量: ${e.touches.length}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -676,12 +686,34 @@
|
|||||||
if (!canvasInitialized) return;
|
if (!canvasInitialized) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const touch = e.changedTouches[0];
|
// 處理所有結束的觸控點
|
||||||
|
for (let i = 0; i < e.changedTouches.length; i++) {
|
||||||
|
const touch = e.changedTouches[i];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connection.invoke("HandleTouchUp", touch.identifier);
|
await connection.invoke("HandleTouchUp", touch.identifier);
|
||||||
} catch (err) {
|
console.log(`觸控結束 [${touch.identifier}]`);
|
||||||
console.error("觸摸結束處理失敗:", err);
|
} catch (err) {
|
||||||
|
console.error("觸摸結束處理失敗:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加觸控取消事件處理
|
||||||
|
canvas.addEventListener('touchcancel', async (e) => {
|
||||||
|
if (!canvasInitialized) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
// 處理所有取消的觸控點
|
||||||
|
for (let i = 0; i < e.changedTouches.length; i++) {
|
||||||
|
const touch = e.changedTouches[i];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.invoke("HandleTouchUp", touch.identifier);
|
||||||
|
console.log(`觸控取消 [${touch.identifier}]`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("觸摸取消處理失敗:", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -1,325 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-TW">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Vue.js Inline Rendering Canvas Demo</title>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@microsoft/signalr@latest/dist/browser/signalr.min.js"></script>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: white;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvas-wrapper {
|
|
||||||
position: relative;
|
|
||||||
height: 600px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
margin: 20px 0;
|
|
||||||
background: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rendering-canvas {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: block;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rendering-canvas:active {
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-button {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: 1px solid #2196F3;
|
|
||||||
background: white;
|
|
||||||
color: #2196F3;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-button:hover {
|
|
||||||
background: #2196F3;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status.connected {
|
|
||||||
background: #e8f5e9;
|
|
||||||
color: #2e7d32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status.disconnected {
|
|
||||||
background: #ffebee;
|
|
||||||
color: #c62828;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<div class="demo-container">
|
|
||||||
<h1>Vue.js 內嵌式渲染畫布示範</h1>
|
|
||||||
|
|
||||||
<div class="status" :class="isConnected ? 'connected' : 'disconnected'">
|
|
||||||
{{ isConnected ? '已連接' : '未連接' }}
|
|
||||||
<span v-if="sessionId">- Session: {{ sessionId }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<button class="view-button" @click="setView('front')">前視圖</button>
|
|
||||||
<button class="view-button" @click="setView('back')">後視圖</button>
|
|
||||||
<button class="view-button" @click="setView('left')">左視圖</button>
|
|
||||||
<button class="view-button" @click="setView('right')">右視圖</button>
|
|
||||||
<button class="view-button" @click="setView('top')">頂視圖</button>
|
|
||||||
<button class="view-button" @click="setView('bottom')">底視圖</button>
|
|
||||||
<button class="view-button" @click="setView('isometric')">等角視圖</button>
|
|
||||||
<button class="view-button" @click="setView('home')">主視圖</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="canvas-wrapper">
|
|
||||||
<canvas ref="canvas" class="rendering-canvas"></canvas>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>提示:使用滑鼠拖曳旋轉視圖,滾輪縮放,鍵盤 F1-F4 切換視圖</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const { createApp } = Vue;
|
|
||||||
|
|
||||||
createApp({
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
connection: null,
|
|
||||||
sessionId: null,
|
|
||||||
isConnected: false,
|
|
||||||
canvas: null,
|
|
||||||
ctx: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.canvas = this.$refs.canvas;
|
|
||||||
this.ctx = this.canvas.getContext('2d');
|
|
||||||
this.connect();
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
if (this.connection) {
|
|
||||||
this.connection.stop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async connect() {
|
|
||||||
try {
|
|
||||||
this.connection = new signalR.HubConnectionBuilder()
|
|
||||||
.withUrl("/renderingHub")
|
|
||||||
.configureLogging(signalR.LogLevel.Information)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// 設置消息處理器
|
|
||||||
this.connection.on("ImageUpdate", (compressedData, originalLength, width, height) => {
|
|
||||||
this.handleImageUpdate(compressedData, originalLength, width, height);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.connection.on("CanvasInitialized", (id) => {
|
|
||||||
this.sessionId = id;
|
|
||||||
console.log("畫布初始化完成,會話ID:", id);
|
|
||||||
this.loadTestObjects();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 連接到服務器
|
|
||||||
await this.connection.start();
|
|
||||||
this.isConnected = true;
|
|
||||||
console.log("已連接到服務器");
|
|
||||||
|
|
||||||
// 初始化畫布
|
|
||||||
await this.initializeCanvas();
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error("連接失敗:", err);
|
|
||||||
this.isConnected = false;
|
|
||||||
setTimeout(() => this.connect(), 5000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async initializeCanvas() {
|
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
|
||||||
const width = Math.floor(rect.width);
|
|
||||||
const height = Math.floor(rect.height);
|
|
||||||
|
|
||||||
this.canvas.width = width;
|
|
||||||
this.canvas.height = height;
|
|
||||||
|
|
||||||
await this.connection.invoke("InitializeCanvas", width, height);
|
|
||||||
|
|
||||||
// 設置事件處理
|
|
||||||
this.setupEvents();
|
|
||||||
},
|
|
||||||
|
|
||||||
setupEvents() {
|
|
||||||
// 滑鼠事件
|
|
||||||
let isMouseDown = false;
|
|
||||||
let mouseButton = 0;
|
|
||||||
|
|
||||||
this.canvas.addEventListener('mousedown', async (e) => {
|
|
||||||
isMouseDown = true;
|
|
||||||
mouseButton = e.button;
|
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
await this.connection.invoke("HandleMouseDown", x, y, e.button);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.canvas.addEventListener('mousemove', async (e) => {
|
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
const buttonMask = isMouseDown ? (1 << mouseButton) : 0;
|
|
||||||
await this.connection.invoke("HandleMouseMove", x, y, buttonMask);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.canvas.addEventListener('mouseup', async (e) => {
|
|
||||||
isMouseDown = false;
|
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
await this.connection.invoke("HandleMouseUp", x, y, e.button);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.canvas.addEventListener('wheel', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
await this.connection.invoke("HandleMouseWheel", x, y, e.deltaX, e.deltaY, "chrome");
|
|
||||||
});
|
|
||||||
|
|
||||||
this.canvas.addEventListener('contextmenu', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 鍵盤事件
|
|
||||||
this.canvas.tabIndex = 0;
|
|
||||||
this.canvas.addEventListener('keydown', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
await this.connection.invoke("HandleKeyDown", e.key, e.code, e.ctrlKey, e.shiftKey, e.altKey);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleImageUpdate(compressedData, originalLength, width, height) {
|
|
||||||
try {
|
|
||||||
// 解壓縮數據
|
|
||||||
const imageData = await this.decompressData(compressedData, originalLength);
|
|
||||||
|
|
||||||
// 創建 ImageData 對象並繪製到畫布
|
|
||||||
const imgData = new ImageData(new Uint8ClampedArray(imageData), width, height);
|
|
||||||
this.ctx.putImageData(imgData, 0, 0);
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error("處理圖像更新時出錯:", err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async decompressData(compressedData, originalLength) {
|
|
||||||
let bytes;
|
|
||||||
|
|
||||||
if (typeof compressedData === 'string') {
|
|
||||||
// Base64 字符串轉換
|
|
||||||
const binaryString = atob(compressedData);
|
|
||||||
bytes = new Uint8Array(binaryString.length);
|
|
||||||
for (let i = 0; i < binaryString.length; i++) {
|
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bytes = new Uint8Array(compressedData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 DecompressionStream 解壓縮
|
|
||||||
const stream = new ReadableStream({
|
|
||||||
start(controller) {
|
|
||||||
controller.enqueue(bytes);
|
|
||||||
controller.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const decompressedStream = stream.pipeThrough(new DecompressionStream('gzip'));
|
|
||||||
const reader = decompressedStream.getReader();
|
|
||||||
const chunks = [];
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
chunks.push(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 合併所有塊
|
|
||||||
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
||||||
const result = new Uint8Array(totalLength);
|
|
||||||
let offset = 0;
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
result.set(chunk, offset);
|
|
||||||
offset += chunk.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
async setView(viewType) {
|
|
||||||
if (this.connection && this.connection.state === 'Connected') {
|
|
||||||
try {
|
|
||||||
await this.connection.invoke('SetView', viewType);
|
|
||||||
console.log(`視圖已切換到: ${viewType}`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`切換視圖失敗: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadTestObjects() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/rendering/test-objects/${this.sessionId}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
console.log('測試對象載入成功');
|
|
||||||
} else {
|
|
||||||
console.error('載入測試對象失敗');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('載入測試對象時出錯:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).mount('#app');
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -42,6 +42,13 @@
|
|||||||
></rendering-canvas-view-dropdown>
|
></rendering-canvas-view-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="padding: 10px; background-color: #e3f2fd; color: #1565c0; border: 1px solid #90caf9; border-radius: 4px; margin: 10px 0;">
|
||||||
|
<strong>操作說明:</strong>
|
||||||
|
<br><strong>滑鼠:</strong>左鍵拖動旋轉,右鍵拖動平移,滾輪縮放
|
||||||
|
<br><strong>鍵盤:</strong>F1-F4切換視圖,方向鍵旋轉,PageUp/PageDown縮放,Home重置視圖
|
||||||
|
<br><strong>觸控:</strong>單指拖動平移,雙指捏合縮放,雙指旋轉,三指上下滑動縮放
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="canvas-wrapper">
|
<div class="canvas-wrapper">
|
||||||
<!-- Self-contained canvas component -->
|
<!-- Self-contained canvas component -->
|
||||||
<rendering-canvas
|
<rendering-canvas
|
||||||
@ -55,7 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import RenderingCanvas from './disp/rendering-canvas.js';
|
import RenderingCanvas from '/_content/hi.webapi/disp/rendering-canvas-vue.js';
|
||||||
import RenderingCanvasViewDropdown from './disp/rendering-canvas-view-dropdown.js';
|
import RenderingCanvasViewDropdown from './disp/rendering-canvas-view-dropdown.js';
|
||||||
|
|
||||||
const { createApp } = Vue;
|
const { createApp } = Vue;
|
||||||
|
193
wwwroot/demo.html
Normal file
193
wwwroot/demo.html
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-TW">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Pure JavaScript RenderingCanvas Demo</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@microsoft/signalr@latest/dist/browser/signalr.min.js"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #1565c0;
|
||||||
|
border: 1px solid #90caf9;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin: 20px 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #2196F3;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button:hover {
|
||||||
|
background-color: #1976D2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#canvas-container {
|
||||||
|
height: 600px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
margin: 20px 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.connected {
|
||||||
|
background-color: #c8e6c9;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
background-color: #ffcdd2;
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Pure JavaScript RenderingCanvas Demo</h1>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<strong>使用說明:</strong>
|
||||||
|
<br>這是使用純 JavaScript 類的 RenderingCanvas 示例
|
||||||
|
<br><strong>滑鼠:</strong>左鍵拖動旋轉,右鍵拖動平移,滾輪縮放
|
||||||
|
<br><strong>鍵盤:</strong>F1-F4切換視圖,方向鍵旋轉,PageUp/PageDown縮放,Home重置視圖
|
||||||
|
<br><strong>觸控:</strong>單指拖動平移,雙指捏合縮放,雙指旋轉,三指上下滑動縮放
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status" class="status">
|
||||||
|
連接狀態:未連接
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button onclick="setView('front')">前視圖</button>
|
||||||
|
<button onclick="setView('back')">後視圖</button>
|
||||||
|
<button onclick="setView('left')">左視圖</button>
|
||||||
|
<button onclick="setView('right')">右視圖</button>
|
||||||
|
<button onclick="setView('top')">頂視圖</button>
|
||||||
|
<button onclick="setView('bottom')">底視圖</button>
|
||||||
|
<button onclick="setView('isometric')">等角視圖</button>
|
||||||
|
<button onclick="setView('home')">主視圖</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="canvas-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/_content/hi.webapi/disp/rendering-canvas.js"></script>
|
||||||
|
<script>
|
||||||
|
let renderingCanvas = null;
|
||||||
|
|
||||||
|
// Initialize when page loads
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
// Create RenderingCanvas instance
|
||||||
|
renderingCanvas = new RenderingCanvas('canvas-container', {
|
||||||
|
autoConnect: true,
|
||||||
|
width: 800,
|
||||||
|
height: 600
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up event handlers
|
||||||
|
renderingCanvas.on('connected', () => {
|
||||||
|
updateStatus('已連接到服務器', 'connected');
|
||||||
|
});
|
||||||
|
|
||||||
|
renderingCanvas.on('disconnected', () => {
|
||||||
|
updateStatus('連接已斷開', '');
|
||||||
|
});
|
||||||
|
|
||||||
|
renderingCanvas.on('serverInitialized', (sessionId) => {
|
||||||
|
console.log('Server initialized with session:', sessionId);
|
||||||
|
loadTestObjects(sessionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
renderingCanvas.on('error', (error) => {
|
||||||
|
updateStatus(`錯誤:${error}`, 'error');
|
||||||
|
console.error('RenderingCanvas error:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
renderingCanvas.on('viewChanged', (viewType) => {
|
||||||
|
console.log('View changed to:', viewType);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up when page unloads
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
if (renderingCanvas) {
|
||||||
|
renderingCanvas.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function updateStatus(message, className) {
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
statusEl.textContent = `連接狀態:${message}`;
|
||||||
|
statusEl.className = 'status ' + className;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setView(viewType) {
|
||||||
|
if (renderingCanvas) {
|
||||||
|
renderingCanvas.setView(viewType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTestObjects(sessionId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/rendering/test-objects/${sessionId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
console.log('Test objects loaded successfully');
|
||||||
|
} else {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error(`Failed to load test objects: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error loading test objects: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,38 +0,0 @@
|
|||||||
/* Rendering Canvas Component Styles */
|
|
||||||
.canvas-container {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvas-container canvas {
|
|
||||||
background-color: rgba(204, 204, 204, 0.5);
|
|
||||||
touch-action: none;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: none;
|
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
display: block;
|
|
||||||
cursor: grab;
|
|
||||||
outline: none;
|
|
||||||
transition: box-shadow 0.3s, border-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvas-container canvas:focus {
|
|
||||||
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvas-container canvas:active {
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Context menu disabled */
|
|
||||||
.canvas-container canvas {
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
@ -1,490 +0,0 @@
|
|||||||
// Add CSS link to the document if not already present
|
|
||||||
if (!document.querySelector('link[href*="rendering-canvas.css"]')) {
|
|
||||||
const link = document.createElement('link');
|
|
||||||
link.rel = 'stylesheet';
|
|
||||||
link.href = './disp/rendering-canvas.css';
|
|
||||||
document.head.appendChild(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'RenderingCanvas',
|
|
||||||
template: `
|
|
||||||
<div class="canvas-container">
|
|
||||||
<canvas
|
|
||||||
ref="renderCanvas"
|
|
||||||
:id="canvasId"
|
|
||||||
tabindex="0"
|
|
||||||
@contextmenu.prevent
|
|
||||||
@mousedown="handleMouseDown"
|
|
||||||
@mousemove="handleMouseMove"
|
|
||||||
@mouseup="handleMouseUp"
|
|
||||||
@wheel.prevent="handleWheel"
|
|
||||||
@keydown="handleKeyDown"
|
|
||||||
@keyup="handleKeyUp"
|
|
||||||
@touchstart.prevent="handleTouchStart"
|
|
||||||
@touchmove.prevent="handleTouchMove"
|
|
||||||
@touchend.prevent="handleTouchEnd"
|
|
||||||
@click="focusCanvas"
|
|
||||||
@focus="onFocus"
|
|
||||||
@blur="onBlur"
|
|
||||||
></canvas>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
props: {
|
|
||||||
hubUrl: {
|
|
||||||
type: String,
|
|
||||||
default: '/renderingHub'
|
|
||||||
},
|
|
||||||
autoConnect: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
|
|
||||||
width: {
|
|
||||||
type: Number,
|
|
||||||
default: 800
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: Number,
|
|
||||||
default: 600
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
canvasId: `rendering-canvas-${Math.random().toString(36).substr(2, 9)}`,
|
|
||||||
connection: null,
|
|
||||||
sessionId: null,
|
|
||||||
isConnected: false,
|
|
||||||
isInitialized: false,
|
|
||||||
hasFocus: false,
|
|
||||||
mouseState: {
|
|
||||||
isDown: false,
|
|
||||||
button: 0,
|
|
||||||
lastX: 0,
|
|
||||||
lastY: 0
|
|
||||||
},
|
|
||||||
compressionSupported: typeof DecompressionStream !== 'undefined',
|
|
||||||
isFirefox: navigator.userAgent.toLowerCase().indexOf('firefox') > -1,
|
|
||||||
resizeObserver: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
await this.setupCanvas();
|
|
||||||
this.setupResizeObserver();
|
|
||||||
|
|
||||||
if (this.autoConnect) {
|
|
||||||
await this.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add visibility change listener
|
|
||||||
this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
|
|
||||||
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|
||||||
},
|
|
||||||
async beforeUnmount() {
|
|
||||||
// Remove visibility change listener
|
|
||||||
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
|
||||||
|
|
||||||
if (this.resizeObserver) {
|
|
||||||
this.resizeObserver.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.connection) {
|
|
||||||
await this.disconnect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
// No longer needed since we manage connection internally
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async connect() {
|
|
||||||
try {
|
|
||||||
// Create SignalR connection
|
|
||||||
this.connection = new signalR.HubConnectionBuilder()
|
|
||||||
.withUrl(this.hubUrl)
|
|
||||||
.configureLogging(signalR.LogLevel.Information)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Set up event handlers
|
|
||||||
this.connection.on("ImageUpdate", (compressedData, originalLength, width, height) => {
|
|
||||||
this.handleImageUpdate(compressedData, originalLength, width, height);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.connection.on("CanvasInitialized", (id) => {
|
|
||||||
this.sessionId = id;
|
|
||||||
this.$emit('serverInitialized', id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start connection
|
|
||||||
await this.connection.start();
|
|
||||||
this.isConnected = true;
|
|
||||||
this.$emit('connected');
|
|
||||||
|
|
||||||
// Initialize server canvas
|
|
||||||
const canvas = this.$refs.renderCanvas;
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
const width = Math.floor(rect.width);
|
|
||||||
const height = Math.floor(rect.height);
|
|
||||||
|
|
||||||
await this.connection.invoke("InitializeCanvas", width, height);
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
this.$emit('error', `Connection failed: ${err}`);
|
|
||||||
console.error('Connection failed:', err);
|
|
||||||
|
|
||||||
// Retry after 5 seconds
|
|
||||||
setTimeout(() => this.connect(), 5000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async disconnect() {
|
|
||||||
if (this.connection) {
|
|
||||||
try {
|
|
||||||
await this.connection.stop();
|
|
||||||
this.connection = null;
|
|
||||||
this.sessionId = null;
|
|
||||||
this.isConnected = false;
|
|
||||||
|
|
||||||
// Clear canvas
|
|
||||||
const canvas = this.$refs.renderCanvas;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
this.$emit('disconnected');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Disconnect failed:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
async setupCanvas() {
|
|
||||||
const canvas = this.$refs.renderCanvas;
|
|
||||||
canvas.width = this.width;
|
|
||||||
canvas.height = this.height;
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
|
||||||
this.$emit('initialized', this.canvasId);
|
|
||||||
},
|
|
||||||
|
|
||||||
setupResizeObserver() {
|
|
||||||
this.resizeObserver = new ResizeObserver(entries => {
|
|
||||||
for (let entry of entries) {
|
|
||||||
const { width, height } = entry.contentRect;
|
|
||||||
this.handleResize(width, height);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(this.$refs.renderCanvas.parentElement);
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleResize(width, height) {
|
|
||||||
const canvas = this.$refs.renderCanvas;
|
|
||||||
canvas.width = Math.floor(width);
|
|
||||||
canvas.height = Math.floor(height);
|
|
||||||
|
|
||||||
if (this.connection && this.connection.state === 'Connected') {
|
|
||||||
try {
|
|
||||||
await this.connection.invoke("HandleResize", canvas.width, canvas.height);
|
|
||||||
} catch (err) {
|
|
||||||
this.$emit('error', `Resize failed: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleVisibilityChange() {
|
|
||||||
if (this.connection && this.connection.state === 'Connected') {
|
|
||||||
try {
|
|
||||||
await this.connection.invoke("HandleVisibilityChange", document.visibilityState);
|
|
||||||
this.$emit('visibilityChanged', document.visibilityState);
|
|
||||||
} catch (err) {
|
|
||||||
this.$emit('error', `Visibility change failed: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
focusCanvas() {
|
|
||||||
this.$refs.renderCanvas.focus();
|
|
||||||
},
|
|
||||||
|
|
||||||
onFocus() {
|
|
||||||
this.hasFocus = true;
|
|
||||||
this.$emit('focus');
|
|
||||||
},
|
|
||||||
|
|
||||||
onBlur() {
|
|
||||||
this.hasFocus = false;
|
|
||||||
this.$emit('blur');
|
|
||||||
},
|
|
||||||
|
|
||||||
// Mouse event handlers
|
|
||||||
async handleMouseDown(e) {
|
|
||||||
if (!this.isInitialized || !this.connection) return;
|
|
||||||
|
|
||||||
this.mouseState.isDown = true;
|
|
||||||
this.mouseState.button = e.button;
|
|
||||||
|
|
||||||
const rect = this.$refs.renderCanvas.getBoundingClientRect();
|
|
||||||
this.mouseState.lastX = e.clientX - rect.left;
|
|
||||||
this.mouseState.lastY = e.clientY - rect.top;
|
|
||||||
|
|
||||||
if (this.connection.state === 'Connected') {
|
|
||||||
try {
|
|
||||||
await this.connection.invoke("HandleMouseDown",
|
|
||||||
this.mouseState.lastX,
|
|
||||||
this.mouseState.lastY,
|
|
||||||
e.button
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
this.$emit('error', `Mouse down failed: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleMouseMove(e) {
|
|
||||||
if (!this.isInitialized) return;
|
|
||||||
|
|
||||||
const rect = this.$refs.renderCanvas.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
|
|
||||||
const buttonMask = this.mouseState.isDown ? (1 << this.mouseState.button) : 0;
|
|
||||||
|
|
||||||
if (this.connection && this.connection.state === 'Connected') {
|
|
||||||
try {
|
|
||||||
await this.connection.invoke("HandleMouseMove", x, y, buttonMask);
|
|
||||||
} catch (err) {
|
|
||||||
this.$emit('error', `Mouse move failed: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mouseState.lastX = x;
|
|
||||||
this.mouseState.lastY = y;
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleMouseUp(e) {
|
|
||||||
if (!this.isInitialized) return;
|
|
||||||
|
|
||||||
this.mouseState.isDown = false;
|
|
||||||
|
|
||||||
const rect = this.$refs.renderCanvas.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
|
|
||||||
if (this.connection && this.connection.state === 'Connected') {
|
|
||||||
try {
|
|
||||||
await this.connection.invoke("HandleMouseUp", x, y, e.button);
|
|
||||||
} catch (err) {
|
|
||||||
this.$emit('error', `Mouse up failed: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleWheel(e) {
|
|
||||||
if (!this.isInitialized) return;
|
|
||||||
|
|
||||||
const rect = this.$refs.renderCanvas.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
|
|
||||||
const browserBrand = this.isFirefox ? 'firefox' : 'chrome';
|
|
||||||
|
|
||||||
if (this.connection && this.connection.state === 'Connected') {
|
|
||||||
try {
|
|
||||||
await this.connection.invoke("HandleMouseWheel",
|
|
||||||
x, y, e.deltaX, e.deltaY, browserBrand
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
this.$emit('error', `Mouse wheel failed: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Keyboard event handlers
|
|
||||||
async handleKeyDown(e) {
|
|
||||||
if (!this.isInitialized) return;
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (this.connection && this.connection.state === 'Connected') {
|
|
||||||
try {
|
|
||||||
await this.connection.invoke("HandleKeyDown",
|
|
||||||
e.key, e.code, e.ctrlKey, e.shiftKey, e.altKey
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
this.$emit('error', `Key down failed: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleKeyUp(e) {
|
|
||||||
if (!this.isInitialized) return;
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (this.connection && this.connection.state === 'Connected') {
|
|
||||||
try {
|
|
||||||
await this.connection.invoke("HandleKeyUp",
|
|
||||||
e.key, e.code, e.ctrlKey, e.shiftKey, e.altKey
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
this.$emit('error', `Key up failed: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Touch event handlers
|
|
||||||
async handleTouchStart(e) {
|
|
||||||
if (!this.isInitialized) return;
|
|
||||||
|
|
||||||
const touch = e.touches[0];
|
|
||||||
const rect = this.$refs.renderCanvas.getBoundingClientRect();
|
|
||||||
const x = touch.clientX - rect.left;
|
|
||||||
const y = touch.clientY - rect.top;
|
|
||||||
|
|
||||||
if (this.connection && this.connection.state === 'Connected') {
|
|
||||||
try {
|
|
||||||
await this.connection.invoke("HandleTouchDown", touch.identifier, x, y);
|
|
||||||
} catch (err) {
|
|
||||||
this.$emit('error', `Touch start failed: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleTouchMove(e) {
|
|
||||||
if (!this.isInitialized) return;
|
|
||||||
|
|
||||||
const touch = e.touches[0];
|
|
||||||
const rect = this.$refs.renderCanvas.getBoundingClientRect();
|
|
||||||
const x = touch.clientX - rect.left;
|
|
||||||
const y = touch.clientY - rect.top;
|
|
||||||
|
|
||||||
if (this.connection && this.connection.state === 'Connected') {
|
|
||||||
try {
|
|
||||||
await this.connection.invoke("HandleTouchMove", touch.identifier, x, y);
|
|
||||||
} catch (err) {
|
|
||||||
this.$emit('error', `Touch move failed: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleTouchEnd(e) {
|
|
||||||
if (!this.isInitialized) return;
|
|
||||||
|
|
||||||
const touch = e.changedTouches[0];
|
|
||||||
|
|
||||||
if (this.connection && this.connection.state === 'Connected') {
|
|
||||||
try {
|
|
||||||
await this.connection.invoke("HandleTouchUp", touch.identifier);
|
|
||||||
} catch (err) {
|
|
||||||
this.$emit('error', `Touch end failed: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Image update handler
|
|
||||||
async handleImageUpdate(compressedData, originalLength, width, height) {
|
|
||||||
try {
|
|
||||||
let imageData;
|
|
||||||
|
|
||||||
if (this.compressionSupported) {
|
|
||||||
const decompressed = await this.decompressData(compressedData, originalLength);
|
|
||||||
imageData = new Uint8ClampedArray(decompressed);
|
|
||||||
} else {
|
|
||||||
imageData = new Uint8ClampedArray(compressedData);
|
|
||||||
}
|
|
||||||
|
|
||||||
const canvas = this.$refs.renderCanvas;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
const imgData = new ImageData(imageData, width, height);
|
|
||||||
ctx.putImageData(imgData, 0, 0);
|
|
||||||
|
|
||||||
this.$emit('imageUpdated');
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
this.$emit('error', `Image update failed: ${err}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async decompressData(compressedData, originalLength) {
|
|
||||||
let bytes;
|
|
||||||
|
|
||||||
if (compressedData instanceof Uint8Array) {
|
|
||||||
bytes = compressedData;
|
|
||||||
} else if (compressedData instanceof ArrayBuffer) {
|
|
||||||
bytes = new Uint8Array(compressedData);
|
|
||||||
} else if (typeof compressedData === 'string') {
|
|
||||||
const binaryString = atob(compressedData);
|
|
||||||
bytes = new Uint8Array(binaryString.length);
|
|
||||||
for (let i = 0; i < binaryString.length; i++) {
|
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unknown compressed data type: ${typeof compressedData}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const stream = new ReadableStream({
|
|
||||||
start(controller) {
|
|
||||||
controller.enqueue(bytes);
|
|
||||||
controller.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const decompressedStream = stream.pipeThrough(new DecompressionStream('gzip'));
|
|
||||||
const reader = decompressedStream.getReader();
|
|
||||||
const chunks = [];
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
chunks.push(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
||||||
const result = new Uint8Array(totalLength);
|
|
||||||
let offset = 0;
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
result.set(chunk, offset);
|
|
||||||
offset += chunk.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Public methods that can be called from parent
|
|
||||||
async setView(viewType) {
|
|
||||||
if (this.connection && this.connection.state === 'Connected') {
|
|
||||||
try {
|
|
||||||
await this.connection.invoke('SetView', viewType);
|
|
||||||
this.$emit('viewChanged', viewType);
|
|
||||||
} catch (err) {
|
|
||||||
this.$emit('error', `Set view failed: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Public method to manually connect if autoConnect is false
|
|
||||||
async manualConnect() {
|
|
||||||
if (!this.isConnected) {
|
|
||||||
await this.connect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Public method to manually disconnect
|
|
||||||
async manualDisconnect() {
|
|
||||||
await this.disconnect();
|
|
||||||
},
|
|
||||||
|
|
||||||
// Getters for component state
|
|
||||||
getConnectionState() {
|
|
||||||
return {
|
|
||||||
isConnected: this.isConnected,
|
|
||||||
sessionId: this.sessionId,
|
|
||||||
connectionState: this.connection ? this.connection.state : 'Not initialized'
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get session ID
|
|
||||||
getSessionId() {
|
|
||||||
return this.sessionId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user