325 lines
13 KiB
HTML
325 lines
13 KiB
HTML
<!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> |