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