Hi.Sample.Webapi/wwwroot/demo-vue-inline.html
2025-07-18 20:25:57 +08:00

325 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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