321 lines
12 KiB
TypeScript
321 lines
12 KiB
TypeScript
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
|
|
}
|
|
}
|