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