This commit is contained in:
iambossTC 2025-07-23 15:21:17 +08:00
parent 68c8096686
commit 910236028b
9 changed files with 262 additions and 889 deletions

View File

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Hi.Webapi.Services;
using Hi.Disp;
using Hi.Geom;
using Hi.Webapi.Services;
namespace Sample.Controllers
{

View File

@ -21,12 +21,6 @@
</ItemGroup>
<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="..\Hi.Webapi\Hi.Webapi.csproj" />
</ItemGroup>

View File

@ -2,7 +2,7 @@ using Hi.HiNcKits;
using Hi.Webapi.Hubs;
using Hi.Webapi.Services;
SingleUserApp.AppBegin();
LocalApp.AppBegin();
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
@ -44,7 +44,7 @@ var app = builder.Build();
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
lifetime.ApplicationStopping.Register(() =>
{
SingleUserApp.AppEnd();
LocalApp.AppEnd();
});
// Configure the HTTP request pipeline.

View File

@ -183,7 +183,8 @@
<div style="padding: 10px; background-color: #fff3cd; color: #856404; border: 1px solid #ffeaa7; border-radius: 4px; margin: 10px;">
<strong>提示:</strong>點擊畫布以啟用鍵盤控制。畫布獲得焦點時邊框會變成綠色。
<br>鍵盤控制F1-F4切換視圖方向鍵旋轉視圖PageUp/PageDown縮放Home重置視圖
<br><strong>鍵盤控制:</strong>F1-F4切換視圖方向鍵旋轉視圖PageUp/PageDown縮放Home重置視圖
<br><strong>觸控手勢:</strong>單指拖動平移,雙指捏合縮放,雙指旋轉,三指上下滑動縮放
</div>
<div class="view-controls">
@ -637,30 +638,33 @@
}
function setupTouchEvents() {
let touchStartX = 0;
let touchStartY = 0;
canvas.addEventListener('touchstart', async (e) => {
if (!canvasInitialized) return;
e.preventDefault();
const touch = e.touches[0];
// 處理所有觸控點,支援多點觸控
for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
const rect = canvas.getBoundingClientRect();
touchStartX = touch.clientX - rect.left;
touchStartY = touch.clientY - rect.top;
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
try {
await connection.invoke("HandleTouchDown", touch.identifier, touchStartX, touchStartY);
await connection.invoke("HandleTouchDown", touch.identifier, x, y);
console.log(`觸控開始 [${touch.identifier}]: (${x}, ${y})`);
} catch (err) {
console.error("觸摸開始處理失敗:", err);
}
}
});
canvas.addEventListener('touchmove', async (e) => {
if (!canvasInitialized) return;
e.preventDefault();
const touch = e.touches[0];
// 處理所有移動的觸控點,支援捏合縮放和旋轉手勢
for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
const rect = canvas.getBoundingClientRect();
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
@ -670,19 +674,47 @@
} catch (err) {
console.error("觸摸移動處理失敗:", err);
}
}
// 顯示當前觸控點數量(用於調試)
if (e.touches.length >= 2) {
console.log(`多點觸控中 - 觸控點數量: ${e.touches.length}`);
}
});
canvas.addEventListener('touchend', async (e) => {
if (!canvasInitialized) return;
e.preventDefault();
const touch = e.changedTouches[0];
// 處理所有結束的觸控點
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);
}
}
});
// 添加觸控取消事件處理
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);
}
}
});
}

View File

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

View File

@ -42,6 +42,13 @@
></rendering-canvas-view-dropdown>
</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">
<!-- Self-contained canvas component -->
<rendering-canvas
@ -55,7 +62,7 @@
</div>
<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';
const { createApp } = Vue;

193
wwwroot/demo.html Normal file
View 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>

View File

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

View File

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