import { StateNode } from "tldraw" import { findNonOverlappingPosition } from "@/utils/shapeCollisionUtils" export class FathomMeetingsTool extends StateNode { static override id = "fathom-meetings" static override initial = "idle" static override children = () => [FathomMeetingsIdle] onSelect() { // Don't create a shape immediately when tool is selected // The user will create one by clicking on the canvas (onPointerDown in idle state) console.log('🎯 FathomMeetingsTool parent: tool selected - waiting for user click') } } export class FathomMeetingsIdle extends StateNode { static override id = "idle" tooltipElement?: HTMLDivElement mouseMoveHandler?: (e: MouseEvent) => void isCreatingShape = false // Flag to prevent multiple shapes from being created override onEnter = () => { // Set cursor to cross (looks like +) this.editor.setCursor({ type: "cross", rotation: 0 }) // Create tooltip element this.tooltipElement = document.createElement('div') this.tooltipElement.style.cssText = ` position: fixed; background: rgba(0, 0, 0, 0.85); color: white; padding: 8px 12px; border-radius: 6px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 13px; white-space: nowrap; z-index: 10000; pointer-events: none; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.1); ` this.tooltipElement.textContent = 'Click anywhere to place tool' // Add tooltip to DOM document.body.appendChild(this.tooltipElement) // Function to update tooltip position this.mouseMoveHandler = (e: MouseEvent) => { if (this.tooltipElement) { const x = e.clientX + 15 const y = e.clientY - 35 // Keep tooltip within viewport bounds const rect = this.tooltipElement.getBoundingClientRect() const viewportWidth = window.innerWidth const viewportHeight = window.innerHeight let finalX = x let finalY = y // Adjust if tooltip would go off the right edge if (x + rect.width > viewportWidth) { finalX = e.clientX - rect.width - 15 } // Adjust if tooltip would go off the bottom edge if (y + rect.height > viewportHeight) { finalY = e.clientY - rect.height - 15 } // Ensure tooltip doesn't go off the top or left finalX = Math.max(10, finalX) finalY = Math.max(10, finalY) this.tooltipElement.style.left = `${finalX}px` this.tooltipElement.style.top = `${finalY}px` } } // Add mouse move listener document.addEventListener('mousemove', this.mouseMoveHandler) } override onPointerDown = (info?: any) => { console.log('📍 FathomMeetingsTool: onPointerDown called', { info, fullInfo: JSON.stringify(info) }) // Prevent multiple shapes from being created if user clicks multiple times if (this.isCreatingShape) { console.log('📍 FathomMeetingsTool: Shape creation already in progress, ignoring click') return } // CRITICAL: Only proceed if we have a valid pointer event with a point AND button // This prevents shapes from being created when tool is selected (without a click) // A real click will have both a point and a button property if (!info || !info.point || info.button === undefined) { console.warn('⚠️ FathomMeetingsTool: No valid pointer event (missing point or button) - not creating shape. This is expected when tool is first selected.', { hasInfo: !!info, hasPoint: !!info?.point, hasButton: info?.button !== undefined }) return } // CRITICAL: Ensure this is a primary button click (left mouse button = 0) // This prevents accidental triggers from other pointer events if (info.button !== 0) { console.log('📍 FathomMeetingsTool: Non-primary button click, ignoring', { button: info.button }) return } // CRITICAL: Additional validation - ensure this is a real click on the canvas // Check that the event target is the canvas or a canvas child, not a UI element if (info.target && typeof info.target === 'object') { const target = info.target as HTMLElement // If clicking on UI elements (toolbar, menus, etc), don't create shape if (target.closest('[data-tldraw-ui]') || target.closest('.tlui-menu') || target.closest('.tlui-toolbar') || target.closest('[role="menu"]') || target.closest('[role="toolbar"]')) { console.log('📍 FathomMeetingsTool: Click on UI element, ignoring') return } } // Get the click position in page coordinates // CRITICAL: Only use info.point - don't use fallback values that might be stale // This ensures we only create shapes on actual clicks, not when tool is selected let clickX: number | undefined let clickY: number | undefined // Method 1: Use info.point (screen coordinates) and convert to page - this is the ONLY reliable source if (info.point) { try { const pagePoint = this.editor.screenToPage(info.point) clickX = pagePoint.x clickY = pagePoint.y console.log('📍 FathomMeetingsTool: Using info.point converted to page:', { screen: info.point, page: { x: clickX, y: clickY } }) } catch (e) { console.error('📍 FathomMeetingsTool: Failed to convert info.point to page coordinates', e) } } // CRITICAL: Only create shape if we have valid click coordinates from info.point // Do NOT use fallback values (currentPagePoint/originPagePoint) as they may be stale // This prevents shapes from being created when tool is selected (without a click) if (clickX === undefined || clickY === undefined) { console.warn('⚠️ FathomMeetingsTool: No valid click position from info.point - not creating shape. This is expected when tool is first selected.') return } // Additional validation: ensure coordinates are reasonable (not 0,0 or extreme values) // This catches cases where info.point might exist but has invalid default values const viewport = this.editor.getViewportPageBounds() const reasonableBounds = { minX: viewport.x - viewport.w * 2, // Allow some margin outside viewport maxX: viewport.x + viewport.w * 3, minY: viewport.y - viewport.h * 2, maxY: viewport.y + viewport.h * 3, } if (clickX < reasonableBounds.minX || clickX > reasonableBounds.maxX || clickY < reasonableBounds.minY || clickY > reasonableBounds.maxY) { console.warn('⚠️ FathomMeetingsTool: Click position outside reasonable bounds - not creating shape', { clickX, clickY, bounds: reasonableBounds }) return } // CRITICAL: Final validation - ensure this is a deliberate click, not a programmatic trigger // Check that we have valid, non-zero coordinates (0,0 is often a default/fallback value) if (clickX === 0 && clickY === 0) { console.warn('⚠️ FathomMeetingsTool: Click position is (0,0) - likely not a real click, ignoring') return } // CRITICAL: Only create shape if tool is actually active (not just selected) // Double-check that we're in the idle state and tool is properly selected const currentTool = this.editor.getCurrentToolId() if (currentTool !== 'fathom-meetings') { console.warn('⚠️ FathomMeetingsTool: Tool not active, ignoring click', { currentTool }) return } // Create a new FathomMeetingsBrowser shape at the click location this.createFathomMeetingsBrowserShape(clickX, clickY) } onSelect() { // Don't create a shape immediately when tool is selected // The user will create one by clicking on the canvas (onPointerDown) console.log('🎯 FathomMeetings tool selected - waiting for user click') } override onExit = () => { this.cleanupTooltip() // Reset flag when exiting the tool this.isCreatingShape = false } private cleanupTooltip = () => { // Remove mouse move listener if (this.mouseMoveHandler) { document.removeEventListener('mousemove', this.mouseMoveHandler) this.mouseMoveHandler = undefined } // Remove tooltip element if (this.tooltipElement) { document.body.removeChild(this.tooltipElement) this.tooltipElement = undefined } } private createFathomMeetingsBrowserShape(clickX: number, clickY: number) { // Set flag to prevent multiple shapes from being created this.isCreatingShape = true try { console.log('📍 FathomMeetingsTool: createFathomMeetingsBrowserShape called', { clickX, clickY }) // Store current camera position to prevent it from changing const currentCamera = this.editor.getCamera() this.editor.stopCameraAnimation() // Standardized size: 800x600 const shapeWidth = 800 const shapeHeight = 600 // Position new browser shape at click location (centered on click) const baseX = clickX - shapeWidth / 2 // Center the shape on click const baseY = clickY - shapeHeight / 2 // Center the shape on click console.log('📍 FathomMeetingsTool: Using click position:', { clickX, clickY, baseX, baseY }) // User clicked - ALWAYS use that exact position, no collision detection // This ensures the shape appears exactly where the user clicked const finalX = baseX const finalY = baseY console.log('📍 FathomMeetingsTool: Using click position directly (no collision check):', { clickPosition: { x: clickX, y: clickY }, shapePosition: { x: finalX, y: finalY }, shapeSize: { w: shapeWidth, h: shapeHeight } }) console.log('📍 FathomMeetingsTool: Final position for shape:', { finalX, finalY }) const browserShape = this.editor.createShape({ type: 'FathomMeetingsBrowser', x: finalX, y: finalY, props: { w: shapeWidth, h: shapeHeight, } }) console.log('✅ Created FathomMeetingsBrowser shape:', browserShape.id) // Restore camera position if it changed const newCamera = this.editor.getCamera() if (currentCamera.x !== newCamera.x || currentCamera.y !== newCamera.y || currentCamera.z !== newCamera.z) { this.editor.setCamera(currentCamera, { animation: { duration: 0 } }) } // Select the new shape and switch to select tool immediately // This ensures the tool switches right after shape creation // Ensure shape ID has the "shape:" prefix (required by TLDraw validation) const shapeId = browserShape.id.startsWith('shape:') ? browserShape.id : `shape:${browserShape.id}` const cameraBeforeSelect = this.editor.getCamera() this.editor.stopCameraAnimation() this.editor.setSelectedShapes([shapeId] as any) this.editor.setCurrentTool('select') // Restore camera if it changed during selection const cameraAfterSelect = this.editor.getCamera() if (cameraBeforeSelect.x !== cameraAfterSelect.x || cameraBeforeSelect.y !== cameraAfterSelect.y || cameraBeforeSelect.z !== cameraAfterSelect.z) { this.editor.setCamera(cameraBeforeSelect, { animation: { duration: 0 } }) } // Reset flag after a short delay to allow the tool switch to complete setTimeout(() => { this.isCreatingShape = false }, 200) } catch (error) { console.error('❌ Error creating FathomMeetingsBrowser shape:', error) // Reset flag on error this.isCreatingShape = false throw error // Re-throw to see the full error } } }