cursor pos transform to canvas

This commit is contained in:
hjlarry 2025-09-11 09:07:03 +08:00
parent f091868b7c
commit 53ba6aadff
4 changed files with 51 additions and 25 deletions

View File

@ -27,7 +27,7 @@ import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { useCollaboration } from '@/app/components/workflow/collaboration'
import { collaborationManager } from '@/app/components/workflow/collaboration'
import { fetchWorkflowDraft } from '@/service/workflow'
import { useStoreApi } from 'reactflow'
import { useReactFlow, useStoreApi } from 'reactflow'
type WorkflowMainProps = Pick<WorkflowProps, 'nodes' | 'edges' | 'viewport'>
const WorkflowMain = ({
@ -39,6 +39,7 @@ const WorkflowMain = ({
const workflowStore = useWorkflowStore()
const appId = useStore(s => s.appId)
const containerRef = useRef<HTMLDivElement>(null)
const reactFlow = useReactFlow()
const store = useStoreApi()
const { startCursorTracking, stopCursorTracking, onlineUsers, cursors, isConnected } = useCollaboration(appId, store)
@ -55,12 +56,12 @@ const WorkflowMain = ({
useEffect(() => {
if (containerRef.current)
startCursorTracking(containerRef as React.RefObject<HTMLElement>)
startCursorTracking(containerRef as React.RefObject<HTMLElement>, reactFlow)
return () => {
stopCursorTracking()
}
}, [startCursorTracking, stopCursorTracking])
}, [startCursorTracking, stopCursorTracking, reactFlow])
const handleWorkflowDataUpdate = useCallback((payload: any) => {
const {

View File

@ -1,4 +1,5 @@
import type { FC } from 'react'
import { useViewport } from 'reactflow'
import type { CursorPosition, OnlineUser } from '@/app/components/workflow/collaboration/types'
import { getUserColor } from '../utils/user-color'
@ -13,6 +14,15 @@ const UserCursors: FC<UserCursorsProps> = ({
myUserId,
onlineUsers,
}) => {
const viewport = useViewport()
const convertToScreenCoordinates = (cursor: CursorPosition) => {
// Convert world coordinates to screen coordinates using current viewport
const screenX = cursor.x * viewport.zoom + viewport.x
const screenY = cursor.y * viewport.zoom + viewport.y
return { x: screenX, y: screenY }
}
return (
<>
{Object.entries(cursors || {}).map(([userId, cursor]) => {
@ -22,14 +32,15 @@ const UserCursors: FC<UserCursorsProps> = ({
const userInfo = onlineUsers.find(user => user.user_id === userId)
const userName = userInfo?.username || `User ${userId.slice(-4)}`
const userColor = getUserColor(userId)
const screenPos = convertToScreenCoordinates(cursor)
return (
<div
key={userId}
className="pointer-events-none absolute z-[10000] transition-all duration-150 ease-out"
style={{
left: cursor.x,
top: cursor.y,
left: screenPos.x,
top: screenPos.y,
}}
>
<svg

View File

@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import type { ReactFlowInstance } from 'reactflow'
import { collaborationManager } from '../core/collaboration-manager'
import { CursorService } from '../services/cursor-service'
import type { CollaborationState } from '../types/collaboration'
@ -62,11 +63,11 @@ export function useCollaboration(appId: string, reactFlowStore?: any) {
}
}, [appId, reactFlowStore])
const startCursorTracking = (containerRef: React.RefObject<HTMLElement>) => {
const startCursorTracking = (containerRef: React.RefObject<HTMLElement>, reactFlowInstance?: ReactFlowInstance) => {
if (cursorServiceRef.current) {
cursorServiceRef.current.startTracking(containerRef, (position) => {
collaborationManager.emitCursorMove(position)
})
}, reactFlowInstance)
}
}

View File

@ -1,5 +1,6 @@
import type { RefObject } from 'react'
import type { CursorPosition } from '../types/collaboration'
import type { ReactFlowInstance } from 'reactflow'
export type CursorServiceConfig = {
minMoveDistance?: number
@ -8,6 +9,7 @@ export type CursorServiceConfig = {
export class CursorService {
private containerRef: RefObject<HTMLElement> | null = null
private reactFlowInstance: ReactFlowInstance | null = null
private isTracking = false
private onCursorUpdate: ((cursors: Record<string, CursorPosition>) => void) | null = null
private onEmitPosition: ((position: CursorPosition) => void) | null = null
@ -25,11 +27,13 @@ export class CursorService {
startTracking(
containerRef: RefObject<HTMLElement>,
onEmitPosition: (position: CursorPosition) => void,
reactFlowInstance?: ReactFlowInstance,
): void {
if (this.isTracking) this.stopTracking()
this.containerRef = containerRef
this.onEmitPosition = onEmitPosition
this.reactFlowInstance = reactFlowInstance || null
this.isTracking = true
if (containerRef.current)
@ -41,6 +45,7 @@ export class CursorService {
this.containerRef.current.removeEventListener('mousemove', this.handleMouseMove)
this.containerRef = null
this.reactFlowInstance = null
this.onEmitPosition = null
this.isTracking = false
this.lastPosition = null
@ -59,26 +64,34 @@ export class CursorService {
if (!this.containerRef?.current || !this.onEmitPosition) return
const rect = this.containerRef.current.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top
let x = event.clientX - rect.left
let y = event.clientY - rect.top
if (x >= 0 && y >= 0 && x <= rect.width && y <= rect.height) {
const now = Date.now()
const timeThrottled = now - this.lastEmitTime > this.config.throttleMs
const distanceThrottled = !this.lastPosition
|| (Math.abs(x - this.lastPosition.x) > this.config.minMoveDistance
|| Math.abs(y - this.lastPosition.y) > this.config.minMoveDistance)
// Transform coordinates to ReactFlow world coordinates if ReactFlow instance is available
if (this.reactFlowInstance) {
const viewport = this.reactFlowInstance.getViewport()
// Convert screen coordinates to world coordinates
// World coordinates = (screen coordinates - viewport translation) / zoom
x = (x - viewport.x) / viewport.zoom
y = (y - viewport.y) / viewport.zoom
}
if (timeThrottled && distanceThrottled) {
this.lastPosition = { x, y }
this.lastEmitTime = now
this.onEmitPosition({
x,
y,
userId: '',
timestamp: now,
})
}
// Always emit cursor position (remove boundary check since world coordinates can be negative)
const now = Date.now()
const timeThrottled = now - this.lastEmitTime > this.config.throttleMs
const distanceThrottled = !this.lastPosition
|| (Math.abs(x - this.lastPosition.x) > this.config.minMoveDistance / (this.reactFlowInstance?.getZoom() || 1))
|| (Math.abs(y - this.lastPosition.y) > this.config.minMoveDistance / (this.reactFlowInstance?.getZoom() || 1))
if (timeThrottled && distanceThrottled) {
this.lastPosition = { x, y }
this.lastEmitTime = now
this.onEmitPosition({
x,
y,
userId: '',
timestamp: now,
})
}
}
}