feat: implement Phase 3 line selection with visual feedback
This commit is contained in:
parent
c3292b4f6c
commit
91c516999d
3 changed files with 56 additions and 12 deletions
|
|
@ -103,7 +103,7 @@ const renderCursorCircle = () => {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Phase 3: Line Selection
|
### Phase 3: Line Selection ✅ DONE
|
||||||
|
|
||||||
#### 3.1 Selection State
|
#### 3.1 Selection State
|
||||||
**File**: `src/components/SvgOverlay.tsx`
|
**File**: `src/components/SvgOverlay.tsx`
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export default function SvgOverlay({
|
||||||
const [annotations, setAnnotations] = useState<Annotation[]>([]);
|
const [annotations, setAnnotations] = useState<Annotation[]>([]);
|
||||||
const [drawingLine, setDrawingLine] = useState<{ start: Point; current: Point } | null>(null);
|
const [drawingLine, setDrawingLine] = useState<{ start: Point; current: Point } | null>(null);
|
||||||
const [mousePosition, setMousePosition] = useState<{ x: number; y: number } | null>(null);
|
const [mousePosition, setMousePosition] = useState<{ x: number; y: number } | null>(null);
|
||||||
|
const [selectedLineId, setSelectedLineId] = useState<number | null>(null);
|
||||||
|
|
||||||
// Fetch annotations
|
// Fetch annotations
|
||||||
const fetchAnnotations = async () => {
|
const fetchAnnotations = async () => {
|
||||||
|
|
@ -158,11 +159,40 @@ export default function SvgOverlay({
|
||||||
// Line drawing mode
|
// Line drawing mode
|
||||||
if (activeTool === 'line') {
|
if (activeTool === 'line') {
|
||||||
if (!drawingLine) {
|
if (!drawingLine) {
|
||||||
// First click - start line
|
// First click - check if clicking near existing line to select it
|
||||||
|
const lineAnnotations = annotations.filter((a) => a.label_type === 'line' && a.geometry);
|
||||||
|
let lineSelected = false;
|
||||||
|
|
||||||
|
for (const annotation of lineAnnotations) {
|
||||||
|
if (!annotation.geometry) continue;
|
||||||
|
|
||||||
|
const start = dataToPixel(
|
||||||
|
annotation.geometry.startTime!,
|
||||||
|
annotation.geometry.startPrice!
|
||||||
|
);
|
||||||
|
const end = dataToPixel(annotation.geometry.endTime!, annotation.geometry.endPrice!);
|
||||||
|
|
||||||
|
if (!start || !end) continue;
|
||||||
|
|
||||||
|
// Calculate distance from point to line segment
|
||||||
|
const distance = distanceToLineSegment({ x, y }, start, end);
|
||||||
|
|
||||||
|
if (distance < 10) {
|
||||||
|
// Within 10 pixels - select this line
|
||||||
|
setSelectedLineId(annotation.id);
|
||||||
|
lineSelected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no line was selected, start drawing a new line
|
||||||
|
if (!lineSelected) {
|
||||||
setDrawingLine({
|
setDrawingLine({
|
||||||
start: dataPoint,
|
start: dataPoint,
|
||||||
current: dataPoint,
|
current: dataPoint,
|
||||||
});
|
});
|
||||||
|
setSelectedLineId(null); // Clear selection when starting new line
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Second click - save line
|
// Second click - save line
|
||||||
try {
|
try {
|
||||||
|
|
@ -269,17 +299,27 @@ export default function SvgOverlay({
|
||||||
return Math.sqrt(dx * dx + dy * dy);
|
return Math.sqrt(dx * dx + dy * dy);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle Escape key to cancel line drawing
|
// Handle Escape key to cancel line drawing and deselect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape' && drawingLine) {
|
if (e.key === 'Escape') {
|
||||||
|
if (drawingLine) {
|
||||||
setDrawingLine(null);
|
setDrawingLine(null);
|
||||||
}
|
}
|
||||||
|
if (selectedLineId !== null) {
|
||||||
|
setSelectedLineId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [drawingLine]);
|
}, [drawingLine, selectedLineId]);
|
||||||
|
|
||||||
|
// Deselect line when tool changes
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedLineId(null);
|
||||||
|
}, [activeTool]);
|
||||||
|
|
||||||
// Render line annotations
|
// Render line annotations
|
||||||
const renderLines = () => {
|
const renderLines = () => {
|
||||||
|
|
@ -296,6 +336,8 @@ export default function SvgOverlay({
|
||||||
|
|
||||||
if (!start || !end) return null;
|
if (!start || !end) return null;
|
||||||
|
|
||||||
|
const isSelected = annotation.id === selectedLineId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<line
|
<line
|
||||||
key={annotation.id}
|
key={annotation.id}
|
||||||
|
|
@ -304,8 +346,9 @@ export default function SvgOverlay({
|
||||||
x2={end.x}
|
x2={end.x}
|
||||||
y2={end.y}
|
y2={end.y}
|
||||||
stroke={annotation.color || '#3b82f6'}
|
stroke={annotation.color || '#3b82f6'}
|
||||||
strokeWidth="2"
|
strokeWidth={isSelected ? 3 : 2}
|
||||||
style={{ cursor: activeTool === 'delete' ? 'pointer' : 'default' }}
|
opacity={isSelected ? 1 : 0.85}
|
||||||
|
style={{ cursor: activeTool === 'delete' ? 'pointer' : activeTool === 'line' ? 'pointer' : 'default' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
1
tsconfig.tsbuildinfo
Normal file
1
tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue