124 lines
3.3 KiB
TypeScript
124 lines
3.3 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import * as React from 'react';
|
||
|
|
import { cn } from '@/lib/utils/cn';
|
||
|
|
import type { AutomationPoint as AutomationPointType } from '@/types/automation';
|
||
|
|
|
||
|
|
export interface AutomationPointProps {
|
||
|
|
point: AutomationPointType;
|
||
|
|
x: number; // Pixel position
|
||
|
|
y: number; // Pixel position
|
||
|
|
isSelected?: boolean;
|
||
|
|
onDragStart?: (pointId: string, startX: number, startY: number) => void;
|
||
|
|
onDrag?: (pointId: string, deltaX: number, deltaY: number) => void;
|
||
|
|
onDragEnd?: (pointId: string) => void;
|
||
|
|
onClick?: (pointId: string, event: React.MouseEvent) => void;
|
||
|
|
onDoubleClick?: (pointId: string) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function AutomationPoint({
|
||
|
|
point,
|
||
|
|
x,
|
||
|
|
y,
|
||
|
|
isSelected = false,
|
||
|
|
onDragStart,
|
||
|
|
onDrag,
|
||
|
|
onDragEnd,
|
||
|
|
onClick,
|
||
|
|
onDoubleClick,
|
||
|
|
}: AutomationPointProps) {
|
||
|
|
const [isDragging, setIsDragging] = React.useState(false);
|
||
|
|
const dragStartRef = React.useRef({ x: 0, y: 0 });
|
||
|
|
|
||
|
|
const handleMouseDown = React.useCallback(
|
||
|
|
(e: React.MouseEvent) => {
|
||
|
|
if (e.button !== 0) return; // Only left click
|
||
|
|
|
||
|
|
e.stopPropagation();
|
||
|
|
setIsDragging(true);
|
||
|
|
dragStartRef.current = { x: e.clientX, y: e.clientY };
|
||
|
|
|
||
|
|
if (onDragStart) {
|
||
|
|
onDragStart(point.id, e.clientX, e.clientY);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[point.id, onDragStart]
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleClick = React.useCallback(
|
||
|
|
(e: React.MouseEvent) => {
|
||
|
|
if (!isDragging && onClick) {
|
||
|
|
onClick(point.id, e);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[isDragging, point.id, onClick]
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleDoubleClick = React.useCallback(
|
||
|
|
(e: React.MouseEvent) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
if (onDoubleClick) {
|
||
|
|
onDoubleClick(point.id);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[point.id, onDoubleClick]
|
||
|
|
);
|
||
|
|
|
||
|
|
// Global mouse handlers
|
||
|
|
React.useEffect(() => {
|
||
|
|
if (!isDragging) return;
|
||
|
|
|
||
|
|
const handleMouseMove = (e: MouseEvent) => {
|
||
|
|
if (!isDragging) return;
|
||
|
|
|
||
|
|
const deltaX = e.clientX - dragStartRef.current.x;
|
||
|
|
const deltaY = e.clientY - dragStartRef.current.y;
|
||
|
|
|
||
|
|
if (onDrag) {
|
||
|
|
onDrag(point.id, deltaX, deltaY);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update drag start position for next delta calculation
|
||
|
|
dragStartRef.current = { x: e.clientX, y: e.clientY };
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleMouseUp = () => {
|
||
|
|
if (isDragging) {
|
||
|
|
setIsDragging(false);
|
||
|
|
if (onDragEnd) {
|
||
|
|
onDragEnd(point.id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
window.addEventListener('mousemove', handleMouseMove);
|
||
|
|
window.addEventListener('mouseup', handleMouseUp);
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
window.removeEventListener('mousemove', handleMouseMove);
|
||
|
|
window.removeEventListener('mouseup', handleMouseUp);
|
||
|
|
};
|
||
|
|
}, [isDragging, point.id, onDrag, onDragEnd]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
'absolute rounded-full cursor-pointer transition-all select-none',
|
||
|
|
'hover:scale-125',
|
||
|
|
isDragging ? 'scale-125 z-10' : 'z-0',
|
||
|
|
isSelected
|
||
|
|
? 'w-3 h-3 bg-primary border-2 border-background shadow-lg'
|
||
|
|
: 'w-2.5 h-2.5 bg-primary/80 border border-background shadow-md'
|
||
|
|
)}
|
||
|
|
style={{
|
||
|
|
left: x - (isSelected || isDragging ? 6 : 5),
|
||
|
|
top: y - (isSelected || isDragging ? 6 : 5),
|
||
|
|
}}
|
||
|
|
onMouseDown={handleMouseDown}
|
||
|
|
onClick={handleClick}
|
||
|
|
onDoubleClick={handleDoubleClick}
|
||
|
|
title={`Time: ${point.time.toFixed(3)}s, Value: ${point.value.toFixed(3)}`}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
}
|