docs: add detailed implementation plan for line drawing improvements
This commit is contained in:
parent
daec116aab
commit
5767669b2c
1 changed files with 324 additions and 0 deletions
324
LINE_DRAWING_IMPROVEMENTS.md
Normal file
324
LINE_DRAWING_IMPROVEMENTS.md
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
# Line Drawing Improvements Plan
|
||||
|
||||
## Context
|
||||
The candle annotator app has a working line drawing feature, but it needs enhancements:
|
||||
- Currently only draws blue lines
|
||||
- No way to select or edit lines after creation
|
||||
- Limited visual feedback during drawing
|
||||
- Delete tool works by clicking near line
|
||||
|
||||
## Current State
|
||||
- Line drawing is working: click to start, move mouse (dashed preview), click to finish
|
||||
- Lines are stored in database with geometry (startTime, startPrice, endTime, endPrice)
|
||||
- Lines maintain relative position to candles during zoom/pan (coordinate conversion working)
|
||||
- SVG overlay is properly layered (z-index) and sized (CSS 100%)
|
||||
|
||||
## Approved Design Decisions
|
||||
1. **Color**: Preset color buttons below "Draw Line" button (Option C)
|
||||
2. **Selection**: Lines selectable only when "Draw Line" tool is active (Option B)
|
||||
3. **Editing**: Drag endpoints only - no full line dragging (simplest approach)
|
||||
4. **Visual Feedback**: Add small circle at mouse cursor during drawing
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Color Support
|
||||
|
||||
#### 1.1 Database Schema
|
||||
**File**: `src/lib/db/schema.ts`
|
||||
```typescript
|
||||
// Add to annotations table:
|
||||
color: text('color').default('#3b82f6'), // hex color code
|
||||
```
|
||||
- Need to run migration or recreate database
|
||||
- Default color: `#3b82f6` (blue)
|
||||
|
||||
#### 1.2 Toolbox UI
|
||||
**File**: `src/components/Toolbox.tsx`
|
||||
- Add state: `const [selectedColor, setSelectedColor] = useState('#3b82f6')`
|
||||
- Add preset color buttons below "Draw Line" button
|
||||
- Colors: Red (#ef4444), Green (#22c55e), Blue (#3b82f6), Yellow (#eab308), White (#ffffff)
|
||||
- Active color button should be highlighted (variant='default', others 'outline')
|
||||
- Pass `selectedColor` up to parent via new prop: `onColorChange`
|
||||
|
||||
#### 1.3 Page Component
|
||||
**File**: `src/app/page.tsx`
|
||||
- Add state: `const [selectedColor, setSelectedColor] = useState('#3b82f6')`
|
||||
- Pass to Toolbox: `onColorChange={setSelectedColor}`
|
||||
- Pass to CandleChart: `selectedColor={selectedColor}`
|
||||
|
||||
#### 1.4 Chart Component
|
||||
**File**: `src/components/CandleChart.tsx`
|
||||
- Accept new prop: `selectedColor: string`
|
||||
- Pass to SvgOverlay: `selectedColor={selectedColor}`
|
||||
|
||||
#### 1.5 SVG Overlay - Create with Color
|
||||
**File**: `src/components/SvgOverlay.tsx`
|
||||
- Accept new prop: `selectedColor: string`
|
||||
- In `handleClick` when saving line (second click), add color to POST body:
|
||||
```typescript
|
||||
body: JSON.stringify({
|
||||
timestamp: drawingLine.start.time,
|
||||
label_type: 'line',
|
||||
color: selectedColor, // NEW
|
||||
geometry: { ... }
|
||||
})
|
||||
```
|
||||
|
||||
#### 1.6 SVG Overlay - Render with Color
|
||||
**File**: `src/components/SvgOverlay.tsx`
|
||||
- In `renderLines()`, read color from annotation:
|
||||
```typescript
|
||||
stroke={annotation.color || '#3b82f6'}
|
||||
```
|
||||
|
||||
#### 1.7 API - Handle Color
|
||||
**File**: `src/app/api/annotations/route.ts`
|
||||
- In POST handler, extract `color` from body
|
||||
- Save to database: `color: body.color || '#3b82f6'`
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Visual Feedback
|
||||
|
||||
#### 2.1 Cursor Circle During Drawing
|
||||
**File**: `src/components/SvgOverlay.tsx`
|
||||
- Add new render function:
|
||||
```typescript
|
||||
const renderCursorCircle = () => {
|
||||
if (!drawingLine || !mousePosition) return null;
|
||||
|
||||
return (
|
||||
<circle
|
||||
cx={mousePosition.x}
|
||||
cy={mousePosition.y}
|
||||
r={5}
|
||||
fill="transparent"
|
||||
stroke={selectedColor}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
- Call in SVG return: `{renderCursorCircle()}`
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Line Selection
|
||||
|
||||
#### 3.1 Selection State
|
||||
**File**: `src/components/SvgOverlay.tsx`
|
||||
- Add state: `const [selectedLineId, setSelectedLineId] = useState<number | null>(null)`
|
||||
|
||||
#### 3.2 Click Logic Update
|
||||
**File**: `src/components/SvgOverlay.tsx`
|
||||
- In `handleClick`, when `activeTool === 'line'`:
|
||||
- If NOT drawing line (drawingLine === null):
|
||||
- First check if clicking near existing line
|
||||
- If yes → select it (setSelectedLineId)
|
||||
- If no → start new line
|
||||
- If drawing line:
|
||||
- Finish and save line (existing behavior)
|
||||
|
||||
#### 3.3 Visual Styling for Selected Line
|
||||
**File**: `src/components/SvgOverlay.tsx`
|
||||
- In `renderLines()`, check if line is selected:
|
||||
```typescript
|
||||
const isSelected = annotation.id === selectedLineId;
|
||||
strokeWidth={isSelected ? 3 : 2}
|
||||
opacity={isSelected ? 1 : 0.85}
|
||||
```
|
||||
|
||||
#### 3.4 Deselect on Tool Change or Escape
|
||||
**File**: `src/components/SvgOverlay.tsx`
|
||||
- Reset `selectedLineId` when `activeTool` changes
|
||||
- Add to existing Escape key handler: also clear `selectedLineId`
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Endpoint Dragging
|
||||
|
||||
#### 4.1 Drag State
|
||||
**File**: `src/components/SvgOverlay.tsx`
|
||||
- Add state:
|
||||
```typescript
|
||||
interface DragState {
|
||||
lineId: number;
|
||||
endpoint: 'start' | 'end';
|
||||
originalPoint: Point;
|
||||
}
|
||||
const [dragState, setDragState] = useState<DragState | null>(null);
|
||||
```
|
||||
|
||||
#### 4.2 Render Endpoint Handles
|
||||
**File**: `src/components/SvgOverlay.tsx`
|
||||
- Add new render function:
|
||||
```typescript
|
||||
const renderHandles = () => {
|
||||
if (!selectedLineId) return null;
|
||||
|
||||
const line = annotations.find(a => a.id === selectedLineId);
|
||||
if (!line || !line.geometry) return null;
|
||||
|
||||
const start = dataToPixel(line.geometry.startTime, line.geometry.startPrice);
|
||||
const end = dataToPixel(line.geometry.endTime, line.geometry.endPrice);
|
||||
|
||||
if (!start || !end) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<circle
|
||||
cx={start.x} cy={start.y} r={6}
|
||||
fill="white"
|
||||
stroke={line.color || '#3b82f6'}
|
||||
strokeWidth={2}
|
||||
style={{ cursor: 'move' }}
|
||||
onMouseDown={(e) => handleHandleMouseDown(e, line.id, 'start')}
|
||||
/>
|
||||
<circle
|
||||
cx={end.x} cy={end.y} r={6}
|
||||
fill="white"
|
||||
stroke={line.color || '#3b82f6'}
|
||||
strokeWidth={2}
|
||||
style={{ cursor: 'move' }}
|
||||
onMouseDown={(e) => handleHandleMouseDown(e, line.id, 'end')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### 4.3 Handle Drag Interaction
|
||||
**File**: `src/components/SvgOverlay.tsx`
|
||||
- Implement handlers:
|
||||
```typescript
|
||||
const handleHandleMouseDown = (e: React.MouseEvent, lineId: number, endpoint: 'start' | 'end') => {
|
||||
e.stopPropagation(); // Prevent line click
|
||||
const line = annotations.find(a => a.id === lineId);
|
||||
if (!line?.geometry) return;
|
||||
|
||||
const point = endpoint === 'start'
|
||||
? { time: line.geometry.startTime, price: line.geometry.startPrice }
|
||||
: { time: line.geometry.endTime, price: line.geometry.endPrice };
|
||||
|
||||
setDragState({ lineId, endpoint, originalPoint: point });
|
||||
};
|
||||
```
|
||||
|
||||
#### 4.4 Mouse Move During Drag
|
||||
**File**: `src/components/SvgOverlay.tsx`
|
||||
- Update `handleMouseMove` to handle drag preview:
|
||||
```typescript
|
||||
if (dragState && mousePosition) {
|
||||
const dataPoint = pixelToData(mousePosition.x, mousePosition.y);
|
||||
if (dataPoint) {
|
||||
// Update local preview of line position
|
||||
// (optimistically update annotations array temporarily)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.5 Mouse Up - Save to Database
|
||||
**File**: `src/components/SvgOverlay.tsx`
|
||||
- Add handler:
|
||||
```typescript
|
||||
const handleMouseUp = async () => {
|
||||
if (!dragState || !mousePosition) return;
|
||||
|
||||
const dataPoint = pixelToData(mousePosition.x, mousePosition.y);
|
||||
if (!dataPoint) return;
|
||||
|
||||
const line = annotations.find(a => a.id === dragState.lineId);
|
||||
if (!line?.geometry) return;
|
||||
|
||||
const updatedGeometry = {
|
||||
...line.geometry,
|
||||
...(dragState.endpoint === 'start'
|
||||
? { startTime: dataPoint.time, startPrice: dataPoint.price }
|
||||
: { endTime: dataPoint.time, endPrice: dataPoint.price })
|
||||
};
|
||||
|
||||
await fetch(`/api/annotations/${dragState.lineId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ geometry: updatedGeometry })
|
||||
});
|
||||
|
||||
await fetchAnnotations();
|
||||
setDragState(null);
|
||||
onAnnotationChange?.();
|
||||
};
|
||||
```
|
||||
- Add to SVG: `onMouseUp={handleMouseUp}`
|
||||
|
||||
#### 4.6 API - PATCH Endpoint
|
||||
**File**: `src/app/api/annotations/[id]/route.ts`
|
||||
- Add PATCH handler:
|
||||
```typescript
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { geometry } = body;
|
||||
|
||||
const result = await db
|
||||
.update(annotations)
|
||||
.set({ geometry: JSON.stringify(geometry) })
|
||||
.where(eq(annotations.id, parseInt(params.id)))
|
||||
.returning();
|
||||
|
||||
const updated = result[0];
|
||||
return NextResponse.json({
|
||||
...updated,
|
||||
geometry: updated.geometry ? JSON.parse(updated.geometry) : null
|
||||
});
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to update annotation' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
After implementation, verify:
|
||||
- [ ] Can select different colors before drawing line
|
||||
- [ ] Lines are drawn in selected color
|
||||
- [ ] Existing lines render with their stored color
|
||||
- [ ] Small circle appears at cursor during line drawing
|
||||
- [ ] Can click existing line (in line tool mode) to select it
|
||||
- [ ] Selected line has visual highlight (thicker stroke)
|
||||
- [ ] Selected line shows endpoint handles (white circles)
|
||||
- [ ] Can drag endpoint handles to reposition line
|
||||
- [ ] Line updates in database after dragging
|
||||
- [ ] Lines maintain position during zoom/pan
|
||||
- [ ] Delete tool still works (click near line)
|
||||
- [ ] Can deselect by clicking empty space or pressing Escape
|
||||
- [ ] Switching tools deselects line
|
||||
|
||||
---
|
||||
|
||||
## Files Modified Summary
|
||||
|
||||
1. `src/lib/db/schema.ts` - Add color field
|
||||
2. `src/components/Toolbox.tsx` - Color picker UI
|
||||
3. `src/app/page.tsx` - Pass color state
|
||||
4. `src/components/CandleChart.tsx` - Pass color to overlay
|
||||
5. `src/components/SvgOverlay.tsx` - Selection, dragging, visual feedback
|
||||
6. `src/app/api/annotations/route.ts` - Handle color in POST
|
||||
7. `src/app/api/annotations/[id]/route.ts` - NEW: PATCH handler
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Lines use data coordinates (time/price) so they survive zoom/pan automatically
|
||||
- SVG overlay has z-index and proper sizing - this is working
|
||||
- Current delete functionality works by clicking near line - keep this behavior
|
||||
- Color state is managed at page level, flows down to components
|
||||
- Database migration needed for color field (or recreate dev database)
|
||||
Loading…
Add table
Add a link
Reference in a new issue