diff --git a/js/ui/drawing-tools.js b/js/ui/drawing-tools.js index f347206..f7ed600 100644 --- a/js/ui/drawing-tools.js +++ b/js/ui/drawing-tools.js @@ -35,13 +35,27 @@ export class DrawingManager { color: '#2962ff', width: 2, style: 0, - opacity: 100 + opacity: 100, + text: '', + textColor: '#2962ff', + fontSize: 14, + bold: false, + italic: false, + alignVert: 'top', + alignHorz: 'left' }, vertical_line: { color: '#2962ff', width: 2, style: 0, - opacity: 100 + opacity: 100, + text: '', + textColor: '#2962ff', + fontSize: 14, + bold: false, + italic: false, + alignVert: 'top', + alignHorz: 'left' } }; @@ -454,6 +468,16 @@ export class DrawingManager { return; // Critical: exit here so we don't move measurement points } + if (this.dragMode === 'label') { + const dx = pos.x - this.dragStartPos.x; + const dy = pos.y - this.dragStartPos.y; + if (!d.labelOffset) d.labelOffset = { x: 0, y: 0 }; + d.labelOffset.x += dx; + d.labelOffset.y += dy; + this.dragStartPos = pos; + this.update(); + return; + } if (this.dragMode === 'p1') { d.p1 = { time: pos.time, price: pos.price }; } else if (this.dragMode === 'p2') { @@ -554,10 +578,26 @@ export class DrawingManager { } } else if (d.price !== undefined && d.type === 'horizontal_line') { const dy = this.series.priceToCoordinate(d.price); - if (dy !== null && Math.abs(y - dy) < threshold) return { drawing: d, part: 'all' }; + if (dy !== null && Math.abs(y - dy) < threshold) { + if (d.text && d.labelPos) { + if (x >= d.labelPos.x && x <= d.labelPos.x + d.labelPos.width && + y >= d.labelPos.y && y <= d.labelPos.y + d.labelPos.height) { + return { drawing: d, part: 'label' }; + } + } + return { drawing: d, part: 'all' }; + } } else if (d.time !== undefined && d.type === 'vertical_line') { const dx = this.timeToX(d.time); - if (dx !== null && Math.abs(x - dx) < threshold) return { drawing: d, part: 'all' }; + if (dx !== null && Math.abs(x - dx) < threshold) { + if (d.text && d.labelPos) { + if (x >= d.labelPos.x && x <= d.labelPos.x + d.labelPos.width && + y >= d.labelPos.y && y <= d.labelPos.y + d.labelPos.height) { + return { drawing: d, part: 'label' }; + } + } + return { drawing: d, part: 'all' }; + } } else if (d.time !== undefined && d.price !== undefined) { const dx = this.timeToX(d.time); const dy = this.series.priceToCoordinate(d.price); @@ -796,6 +836,47 @@ export class DrawingManager { ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(0, y, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } + + // Render Text if present + if (d.text) { + ctx.save(); + ctx.setLineDash([]); + const fontSize = d.fontSize || 14; + const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : ''); + ctx.font = `${fontStyle}${fontSize}px Inter`; + ctx.fillStyle = d.textColor || d.color; + const tx = scope.mediaSize.width / 2; + const ty = y; + const offset = 10; + const finalTy = d.alignVert === 'top' ? ty - offset : (d.alignVert === 'bottom' ? ty + offset : ty); + + ctx.textAlign = d.alignHorz || 'center'; + ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom'); + ctx.fillText(d.text, tx, finalTy); + ctx.restore(); + } + + // Store label position for hit detection + if (d.text) { + ctx.save(); + ctx.setLineDash([]); + const fontSize = d.fontSize || 14; + const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : ''); + ctx.font = `${fontStyle}${fontSize}px Inter`; + const metrics = ctx.measureText(d.text); + const labelWidth = metrics.width + 16; + const labelHeight = 24; + const defaultLabelX = scope.mediaSize.width / 2 - labelWidth / 2; + const defaultLabelY = d.alignVert === 'top' ? y - 40 : (d.alignVert === 'bottom' ? y + 20 : y - 12); + d.labelPos = { + x: defaultLabelX + (d.labelOffset?.x || 0), + y: defaultLabelY + (d.labelOffset?.y || 0), + width: labelWidth, + height: labelHeight + }; + ctx.restore(); + } + ctx.restore(); } } else if (d.type === 'vertical_line') { @@ -817,6 +898,47 @@ export class DrawingManager { ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(x, 0, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } + + // Render Text if present + if (d.text) { + ctx.save(); + ctx.setLineDash([]); + const fontSize = d.fontSize || 14; + const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : ''); + ctx.font = `${fontStyle}${fontSize}px Inter`; + ctx.fillStyle = d.textColor || d.color; + const tx = x; + const ty = 0; + const offset = 10; + const finalTy = d.alignVert === 'top' ? ty - offset : (d.alignVert === 'bottom' ? ty + offset : ty); + + ctx.textAlign = d.alignHorz || 'center'; + ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom'); + ctx.fillText(d.text, tx, finalTy); + ctx.restore(); + } + + // Store label position for hit detection + if (d.text) { + ctx.save(); + ctx.setLineDash([]); + const fontSize = d.fontSize || 14; + const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : ''); + ctx.font = `${fontStyle}${fontSize}px Inter`; + const metrics = ctx.measureText(d.text); + const labelWidth = metrics.width + 16; + const labelHeight = 24; + const defaultLabelX = x - labelWidth / 2; + const defaultLabelY = d.alignVert === 'top' ? -50 : (d.alignVert === 'bottom' ? 20 : -12); + d.labelPos = { + x: defaultLabelX + (d.labelOffset?.x || 0), + y: defaultLabelY + (d.labelOffset?.y || 0), + width: labelWidth, + height: labelHeight + }; + ctx.restore(); + } + ctx.restore(); } } else if (d.type === 'fib_retracement') {