tune.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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: `
|
||||
<div class="canvas-container">
|
||||
<canvas
|
||||
ref="renderCanvas"
|
||||
:id="canvasId"
|
||||
tabindex="0"
|
||||
@contextmenu.prevent
|
||||
@mousedown="handleMouseDown"
|
||||
@mousemove="handleMouseMove"
|
||||
@mouseup="handleMouseUp"
|
||||
@wheel.prevent="handleWheel"
|
||||
@keydown="handleKeyDown"
|
||||
@keyup="handleKeyUp"
|
||||
@touchstart.prevent="handleTouchStart"
|
||||
@touchmove.prevent="handleTouchMove"
|
||||
@touchend.prevent="handleTouchEnd"
|
||||
@click="focusCanvas"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
></canvas>
|
||||
</div>
|
||||
`,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user