diff --git a/Controllers/RenderingController.cs b/Controllers/RenderingController.cs index a1a7350..54b512b 100644 --- a/Controllers/RenderingController.cs +++ b/Controllers/RenderingController.cs @@ -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 { diff --git a/Hi.Sample.Webapi.csproj b/Hi.Sample.Webapi.csproj index 115aabf..ad956ad 100644 --- a/Hi.Sample.Webapi.csproj +++ b/Hi.Sample.Webapi.csproj @@ -21,12 +21,6 @@ - - - - - - diff --git a/Program.cs b/Program.cs index f0e6178..e4ee10b 100644 --- a/Program.cs +++ b/Program.cs @@ -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(); lifetime.ApplicationStopping.Register(() => { - SingleUserApp.AppEnd(); + LocalApp.AppEnd(); }); // Configure the HTTP request pipeline. diff --git a/wwwroot/demo-native.html b/wwwroot/demo-plain-inline.html similarity index 89% rename from wwwroot/demo-native.html rename to wwwroot/demo-plain-inline.html index 82882c1..131ab7d 100644 --- a/wwwroot/demo-native.html +++ b/wwwroot/demo-plain-inline.html @@ -183,7 +183,8 @@
提示:點擊畫布以啟用鍵盤控制。畫布獲得焦點時邊框會變成綠色。 -
鍵盤控制:F1-F4切換視圖,方向鍵旋轉視圖,PageUp/PageDown縮放,Home重置視圖 +
鍵盤控制:F1-F4切換視圖,方向鍵旋轉視圖,PageUp/PageDown縮放,Home重置視圖 +
觸控手勢:單指拖動平移,雙指捏合縮放,雙指旋轉,三指上下滑動縮放
@@ -637,22 +638,23 @@ } function setupTouchEvents() { - let touchStartX = 0; - let touchStartY = 0; - canvas.addEventListener('touchstart', async (e) => { if (!canvasInitialized) return; e.preventDefault(); - const touch = e.touches[0]; - const rect = canvas.getBoundingClientRect(); - touchStartX = touch.clientX - rect.left; - touchStartY = touch.clientY - rect.top; - - try { - await connection.invoke("HandleTouchDown", touch.identifier, touchStartX, touchStartY); - } catch (err) { - console.error("觸摸開始處理失敗:", err); + // 處理所有觸控點,支援多點觸控 + 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; + + try { + await connection.invoke("HandleTouchDown", touch.identifier, x, y); + console.log(`觸控開始 [${touch.identifier}]: (${x}, ${y})`); + } catch (err) { + console.error("觸摸開始處理失敗:", err); + } } }); @@ -660,15 +662,23 @@ if (!canvasInitialized) return; e.preventDefault(); - const touch = e.touches[0]; - const rect = canvas.getBoundingClientRect(); - const x = touch.clientX - rect.left; - const y = touch.clientY - rect.top; + // 處理所有移動的觸控點,支援捏合縮放和旋轉手勢 + 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; + + try { + await connection.invoke("HandleTouchMove", touch.identifier, x, y); + } catch (err) { + console.error("觸摸移動處理失敗:", err); + } + } - try { - await connection.invoke("HandleTouchMove", touch.identifier, x, y); - } catch (err) { - console.error("觸摸移動處理失敗:", err); + // 顯示當前觸控點數量(用於調試) + if (e.touches.length >= 2) { + console.log(`多點觸控中 - 觸控點數量: ${e.touches.length}`); } }); @@ -676,12 +686,34 @@ 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; - try { - await connection.invoke("HandleTouchUp", touch.identifier); - } catch (err) { - console.error("觸摸結束處理失敗:", err); + 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); + } } }); } diff --git a/wwwroot/demo-vue-inline.html b/wwwroot/demo-vue-inline.html deleted file mode 100644 index cc484c7..0000000 --- a/wwwroot/demo-vue-inline.html +++ /dev/null @@ -1,325 +0,0 @@ - - - - - - Vue.js Inline Rendering Canvas Demo - - - - - -
-
-

Vue.js 內嵌式渲染畫布示範

- -
- {{ isConnected ? '已連接' : '未連接' }} - - Session: {{ sessionId }} -
- -
- - - - - - - - -
- -
- -
- -

提示:使用滑鼠拖曳旋轉視圖,滾輪縮放,鍵盤 F1-F4 切換視圖

-
-
- - - - \ No newline at end of file diff --git a/wwwroot/demo-vue.html b/wwwroot/demo-vue.html index e08fde9..8527f0a 100644 --- a/wwwroot/demo-vue.html +++ b/wwwroot/demo-vue.html @@ -42,6 +42,13 @@ >
+
+ 操作說明: +
滑鼠:左鍵拖動旋轉,右鍵拖動平移,滾輪縮放 +
鍵盤:F1-F4切換視圖,方向鍵旋轉,PageUp/PageDown縮放,Home重置視圖 +
觸控:單指拖動平移,雙指捏合縮放,雙指旋轉,三指上下滑動縮放 +
+
+ + + +
+

Pure JavaScript RenderingCanvas Demo

+ +
+ 使用說明: +
這是使用純 JavaScript 類的 RenderingCanvas 示例 +
滑鼠:左鍵拖動旋轉,右鍵拖動平移,滾輪縮放 +
鍵盤:F1-F4切換視圖,方向鍵旋轉,PageUp/PageDown縮放,Home重置視圖 +
觸控:單指拖動平移,雙指捏合縮放,雙指旋轉,三指上下滑動縮放 +
+ +
+ 連接狀態:未連接 +
+ +
+ + + + + + + + +
+ +
+
+ + + + + \ No newline at end of file diff --git a/wwwroot/disp/rendering-canvas.css b/wwwroot/disp/rendering-canvas.css deleted file mode 100644 index f18f9b0..0000000 --- a/wwwroot/disp/rendering-canvas.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/wwwroot/disp/rendering-canvas.js b/wwwroot/disp/rendering-canvas.js deleted file mode 100644 index 8be1c54..0000000 --- a/wwwroot/disp/rendering-canvas.js +++ /dev/null @@ -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: ` -
- -
- `, - 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; - } - } -} \ No newline at end of file