feat: Add text support for horizontal and vertical lines

- Add text-related default settings (text, textColor, fontSize, bold, italic, alignVert, alignHorz)
- Support text rendering on horizontal/vertical lines with alignment options
- Add label position storage and hit detection for draggable text labels
This commit is contained in:
DiTus
2026-03-21 23:43:19 +01:00
parent 30ac99479f
commit 09c7657070

View File

@ -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') {