import { StateNode } from "tldraw" import { HolonShape } from "@/shapes/HolonShapeUtil" import { holosphereService } from "@/lib/HoloSphereService" export class HolonTool extends StateNode { static override id = "holon" static override initial = "idle" static override children = () => [HolonIdle] } export class HolonIdle extends StateNode { static override id = "idle" tooltipElement?: HTMLDivElement mouseMoveHandler?: (e: MouseEvent) => void 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) => { // Get the click position in page coordinates // Try multiple methods to ensure we get the correct click position let clickX: number | undefined let clickY: number | undefined // Method 1: Try info.point (screen coordinates) and convert to page if (info?.point) { try { const pagePoint = this.editor.screenToPage(info.point) clickX = pagePoint.x clickY = pagePoint.y console.log('📍 HolonTool: Method 1 - info.point converted:', { screen: info.point, page: { x: clickX, y: clickY } }) } catch (e) { console.log('📍 HolonTool: Failed to convert info.point, trying other methods') } } // Method 2: Use currentPagePoint from editor inputs (most reliable) if (clickX === undefined || clickY === undefined) { const { currentPagePoint } = this.editor.inputs if (currentPagePoint && currentPagePoint.x !== undefined && currentPagePoint.y !== undefined) { clickX = currentPagePoint.x clickY = currentPagePoint.y console.log('📍 HolonTool: Method 2 - currentPagePoint:', { x: clickX, y: clickY }) } } // Method 3: Try originPagePoint as last resort if (clickX === undefined || clickY === undefined) { const { originPagePoint } = this.editor.inputs if (originPagePoint && originPagePoint.x !== undefined && originPagePoint.y !== undefined) { clickX = originPagePoint.x clickY = originPagePoint.y console.log('📍 HolonTool: Method 3 - originPagePoint:', { x: clickX, y: clickY }) } } if (clickX === undefined || clickY === undefined) { console.error('❌ HolonTool: Could not determine click position!', { info, inputs: this.editor.inputs }) } // Create a new Holon shape at the click location this.createHolonShape(clickX, clickY) } override onExit = () => { this.cleanupTooltip() // Clean up event listeners if ((this as any).cleanup) { ;(this as any).cleanup() } } private cleanupTooltip = () => { // Remove mouse move listener if (this.mouseMoveHandler) { document.removeEventListener('mousemove', this.mouseMoveHandler) this.mouseMoveHandler = undefined } // Remove tooltip element (safely check if it exists in DOM) if (this.tooltipElement) { try { if (this.tooltipElement.parentNode) { document.body.removeChild(this.tooltipElement) } } catch (e) { // Element might already be removed, ignore error console.log('Tooltip element already removed') } this.tooltipElement = undefined } } private createHolonShape(clickX?: number, clickY?: number) { try { // Store current camera position to prevent it from changing const currentCamera = this.editor.getCamera() this.editor.stopCameraAnimation() // Standardized size: 700x400 (matches default props to fit ID and button) const shapeWidth = 700 const shapeHeight = 400 // Use click position if available, otherwise fall back to viewport center let baseX: number let baseY: number if (clickX !== undefined && clickY !== undefined) { // Position new Holon shape at click location (centered on click) baseX = clickX - shapeWidth / 2 // Center the shape on click baseY = clickY - shapeHeight / 2 // Center the shape on click console.log('📍 HolonTool: Calculated base position from click:', { clickX, clickY, baseX, baseY, shapeWidth, shapeHeight }) } else { // Fallback to viewport center if no click coordinates const viewport = this.editor.getViewportPageBounds() const centerX = viewport.x + viewport.w / 2 const centerY = viewport.y + viewport.h / 2 baseX = centerX - shapeWidth / 2 // Center the shape baseY = centerY - shapeHeight / 2 // Center the shape } // Find existing Holon shapes for naming const allShapes = this.editor.getCurrentPageShapes() const existingHolonShapes = allShapes.filter(s => s.type === 'Holon') // ALWAYS use click position directly when provided - user clicked where they want it // Skip collision detection entirely for user clicks to ensure it appears exactly where clicked let finalX = baseX let finalY = baseY if (clickX !== undefined && clickY !== undefined) { // User clicked - ALWAYS use that exact position, no collision detection // This ensures the shape appears exactly where the user clicked finalX = baseX finalY = baseY console.log('📍 Using click position directly (no collision check):', { clickPosition: { x: clickX, y: clickY }, shapePosition: { x: finalX, y: finalY }, shapeSize: { w: shapeWidth, h: shapeHeight } }) } else { // For fallback (no click), use base position directly finalX = baseX finalY = baseY console.log('📍 No click position - using base position:', { finalX, finalY }) } // Default coordinates (can be changed by user) const defaultLat = 40.7128 // NYC const defaultLng = -74.0060 const defaultResolution = 7 // City level console.log('📍 HolonTool: Final position for shape:', { finalX, finalY, wasOverlap: clickX !== undefined && clickY !== undefined && (finalX !== baseX || finalY !== baseY) }) const holonShape = this.editor.createShape({ type: 'Holon', x: finalX, y: finalY, props: { w: shapeWidth, h: shapeHeight, name: `Holon ${existingHolonShapes.length + 1}`, description: '', latitude: defaultLat, longitude: defaultLng, resolution: defaultResolution, holonId: '', isConnected: false, isEditing: true, selectedLens: 'general', data: {}, connections: [], lastUpdated: Date.now() } }) console.log('✅ Created Holon shape:', holonShape.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 } }) } // Don't select the new shape - let it be created without selection like other tools // Clean up tooltip before switching tools this.cleanupTooltip() // Switch back to selector tool after creating the shape this.editor.setCurrentTool('select') } catch (error) { console.error('❌ Error creating Holon shape:', error) } } onSelect() { // Check if there are existing Holon shapes on the canvas const allShapes = this.editor.getCurrentPageShapes() const holonShapes = allShapes.filter(shape => shape.type === 'Holon') if (holonShapes.length > 0) { // If Holon shapes exist, select them and center the view this.editor.setSelectedShapes(holonShapes.map(shape => shape.id)) this.editor.zoomToFit() console.log('🎯 Holon tool selected - showing existing Holon shapes:', holonShapes.length) // Add refresh all functionality this.addRefreshAllListener() } else { // If no Holon shapes exist, don't automatically create one // The user will create one by clicking on the canvas (onPointerDown) console.log('🎯 Holon tool selected - no Holon shapes found, waiting for user interaction') } } private addRefreshAllListener() { // Listen for refresh-all-holons event const handleRefreshAll = async () => { console.log('🔄 Refreshing all Holon shapes...') const shapeUtil = new HolonShape(this.editor) shapeUtil.editor = this.editor const allShapes = this.editor.getCurrentPageShapes() const holonShapes = allShapes.filter(shape => shape.type === 'Holon') let successCount = 0 let failCount = 0 for (const shape of holonShapes) { try { // Trigger a refresh for each Holon shape const event = new CustomEvent('refresh-holon', { detail: { shapeId: shape.id } }) window.dispatchEvent(event) successCount++ } catch (error) { console.error(`❌ Failed to refresh Holon ${shape.id}:`, error) failCount++ } } if (successCount > 0) { alert(`✅ Refreshed ${successCount} Holon shapes!${failCount > 0 ? ` (${failCount} failed)` : ''}`) } else { alert('❌ Failed to refresh any Holon shapes. Check console for details.') } } window.addEventListener('refresh-all-holons', handleRefreshAll) // Clean up listener when tool is deselected const cleanup = () => { window.removeEventListener('refresh-all-holons', handleRefreshAll) } // Store cleanup function for later use ;(this as any).cleanup = cleanup } }