This commit is contained in:
2025-07-18 20:25:57 +08:00
commit 68c8096686
15 changed files with 2582 additions and 0 deletions
+325
View File
@@ -0,0 +1,325 @@
<!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>