fix: market drawing tools overlay alignment and pointer handling

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-08 12:45:47 +08:00
parent 26a4c04b88
commit ef57872d14
4 changed files with 150 additions and 76 deletions
+8 -2
View File
@@ -2909,17 +2909,23 @@ body.login-page {
min-width: 0; min-width: 0;
height: 100%; height: 100%;
position: relative; position: relative;
overflow: hidden;
} }
.market-draw-canvas { .market-draw-canvas {
position: absolute; position: absolute;
inset: 0; top: 0;
z-index: 3; left: 0;
width: 100%;
height: 100%;
z-index: 20;
pointer-events: none; pointer-events: none;
touch-action: none;
} }
.market-draw-canvas.is-drawing { .market-draw-canvas.is-drawing {
cursor: crosshair; cursor: crosshair;
pointer-events: auto;
} }
.market-exchange-badge { .market-exchange-badge {
+3
View File
@@ -247,6 +247,9 @@
mainEl: elChartMain, mainEl: elChartMain,
canvasEl: elDrawCanvas, canvasEl: elDrawCanvas,
toolbarEl: elDrawToolbar, toolbarEl: elDrawToolbar,
getCandles: function () {
return lastCandles;
},
}); });
window.HubChartDraw.setViewKey(currentChartViewKey()); window.HubChartDraw.setViewKey(currentChartViewKey());
drawAttached = true; drawAttached = true;
+136 -71
View File
@@ -35,6 +35,8 @@
let selectedId = null; let selectedId = null;
let redrawRaf = 0; let redrawRaf = 0;
let unsubRange = null; let unsubRange = null;
let getCandlesFn = null;
let brushPointerId = null;
function uid() { function uid() {
return "d" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7); return "d" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
@@ -93,10 +95,19 @@
if (ctx) ctx.setTransform(dpr, 0, 0, dpr, 0, 0); if (ctx) ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
} }
function getCandles() {
if (typeof getCandlesFn === "function") {
const rows = getCandlesFn();
return Array.isArray(rows) ? rows : [];
}
return [];
}
function timeToX(time) { function timeToX(time) {
if (!chart || time == null) return null; if (!chart || time == null) return null;
try { try {
return chart.timeScale().timeToCoordinate(time); const x = chart.timeScale().timeToCoordinate(time);
return x == null || !Number.isFinite(x) ? null : x;
} catch (_) { } catch (_) {
return null; return null;
} }
@@ -105,29 +116,87 @@
function priceToY(price) { function priceToY(price) {
if (!series || price == null || !Number.isFinite(Number(price))) return null; if (!series || price == null || !Number.isFinite(Number(price))) return null;
try { try {
return series.priceToCoordinate(Number(price)); const y = series.priceToCoordinate(Number(price));
return y == null || !Number.isFinite(y) ? null : y;
} catch (_) { } catch (_) {
return null; return null;
} }
} }
function xToTime(x) {
if (!chart) return null;
try {
const direct = chart.timeScale().coordinateToTime(x);
if (direct != null) return direct;
} catch (_) {}
const candles = getCandles();
if (!candles.length) return null;
let bestTime = candles[0].time;
let bestDist = Infinity;
candles.forEach(function (c) {
const cx = timeToX(c.time);
if (cx == null) return;
const d = Math.abs(cx - x);
if (d < bestDist) {
bestDist = d;
bestTime = c.time;
}
});
return bestTime;
}
function yToPrice(y) {
if (!series) return null;
try {
const direct = series.coordinateToPrice(y);
if (direct != null && Number.isFinite(Number(direct))) return Number(direct);
} catch (_) {}
const candles = getCandles();
if (!candles.length) return null;
let lo = null;
let hi = null;
candles.forEach(function (c) {
const vals = [c.low, c.high, c.open, c.close];
vals.forEach(function (v) {
const n = Number(v);
if (!Number.isFinite(n)) return;
if (lo == null || n < lo) lo = n;
if (hi == null || n > hi) hi = n;
});
});
if (lo == null || hi == null) return null;
const yLo = priceToY(lo);
const yHi = priceToY(hi);
if (yLo == null || yHi == null || Math.abs(yHi - yLo) < 1e-6) return (lo + hi) / 2;
const ratio = (y - yLo) / (yHi - yLo);
return lo + (hi - lo) * ratio;
}
function xyToPoint(x, y) { function xyToPoint(x, y) {
if (!chart || !series) return null; if (!chart || !series) return null;
let time = null; const time = xToTime(x);
let price = null; const price = yToPrice(y);
try { if (time == null || price == null || !Number.isFinite(price)) return null;
time = chart.timeScale().coordinateToTime(x); return { time: time, price: price };
price = series.coordinateToPrice(y);
} catch (_) {}
if (time == null || price == null || !Number.isFinite(Number(price))) return null;
return { time: time, price: Number(price) };
} }
function clientToLocal(ev) { function clientToLocal(ev) {
const rect = canvasEl.getBoundingClientRect(); const rect = (hostEl || canvasEl).getBoundingClientRect();
return { x: ev.clientX - rect.left, y: ev.clientY - rect.top }; return { x: ev.clientX - rect.left, y: ev.clientY - rect.top };
} }
function mountCanvasOverlay() {
if (!canvasEl || !hostEl) return;
if (canvasEl.parentElement !== hostEl) {
hostEl.appendChild(canvasEl);
}
canvasEl.style.position = "absolute";
canvasEl.style.top = "0";
canvasEl.style.left = "0";
canvasEl.style.width = "100%";
canvasEl.style.height = "100%";
}
function scheduleRedraw() { function scheduleRedraw() {
if (redrawRaf) cancelAnimationFrame(redrawRaf); if (redrawRaf) cancelAnimationFrame(redrawRaf);
redrawRaf = requestAnimationFrame(function () { redrawRaf = requestAnimationFrame(function () {
@@ -415,13 +484,36 @@
return 0; return 0;
} }
function tryEraseAt(x, y) {
for (let i = drawings.length - 1; i >= 0; i--) {
if (hitTestDrawing(drawings[i], x, y)) {
drawings.splice(i, 1);
selectedId = null;
saveDrawings();
scheduleRedraw();
return true;
}
}
return false;
}
function onPointerDown(ev) { function onPointerDown(ev) {
if (activeTool === "cursor" || activeTool === "clear" || activeTool === "erase") return; if (activeTool === "cursor" || activeTool === "clear") return;
if (!chart || !series) return; if (!chart || !series || !canvasEl) return;
const loc = clientToLocal(ev); const loc = clientToLocal(ev);
if (activeTool === "erase") {
tryEraseAt(loc.x, loc.y);
ev.preventDefault();
return;
}
const pt = xyToPoint(loc.x, loc.y); const pt = xyToPoint(loc.x, loc.y);
if (!pt) return; if (!pt) return;
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation();
try {
canvasEl.setPointerCapture(ev.pointerId);
brushPointerId = ev.pointerId;
} catch (_) {}
if (activeTool === "brush") { if (activeTool === "brush") {
draft = { type: "brush", points: [pt] }; draft = { type: "brush", points: [pt] };
@@ -467,22 +559,26 @@
} }
function onPointerMove(ev) { function onPointerMove(ev) {
if (!draft) return; if (activeTool === "brush" && draft && draft.type === "brush") {
const loc = clientToLocal(ev); const loc = clientToLocal(ev);
const pt = xyToPoint(loc.x, loc.y); const pt = xyToPoint(loc.x, loc.y);
if (!pt) return; if (!pt) return;
if (activeTool === "brush" && draft.type === "brush") {
const last = draft.points[draft.points.length - 1]; const last = draft.points[draft.points.length - 1];
const lx = timeToX(last.time); const lx = timeToX(last.time);
const ly = priceToY(last.price); const ly = priceToY(last.price);
const cx = timeToX(pt.time); const cx = timeToX(pt.time);
const cy = priceToY(pt.price); const cy = priceToY(pt.price);
if (lx != null && ly != null && cx != null && cy != null && Math.hypot(cx - lx, cy - ly) > 3) { if (lx != null && ly != null && cx != null && cy != null && Math.hypot(cx - lx, cy - ly) > 2) {
draft.points.push(pt); draft.points.push(pt);
scheduleRedraw(); scheduleRedraw();
} }
ev.preventDefault();
return; return;
} }
if (!draft) return;
const loc = clientToLocal(ev);
const pt = xyToPoint(loc.x, loc.y);
if (!pt) return;
if (pointsNeeded(activeTool) >= 2 && draft.points.length >= 1) { if (pointsNeeded(activeTool) >= 2 && draft.points.length >= 1) {
draft.points[draft.points.length] = pt; draft.points[draft.points.length] = pt;
if (draft.points.length > pointsNeeded(activeTool)) { if (draft.points.length > pointsNeeded(activeTool)) {
@@ -493,6 +589,12 @@
} }
function onPointerUp(ev) { function onPointerUp(ev) {
if (brushPointerId != null) {
try {
canvasEl.releasePointerCapture(brushPointerId);
} catch (_) {}
brushPointerId = null;
}
if (activeTool === "brush" && draft && draft.type === "brush" && draft.points.length > 1) { if (activeTool === "brush" && draft && draft.type === "brush" && draft.points.length > 1) {
commitDrawing(draft); commitDrawing(draft);
} }
@@ -569,44 +671,11 @@
} }
} }
function passthroughPointerToChart(ev) { function syncCanvasPointerMode() {
if (!hostEl) return; if (!canvasEl) return;
canvasEl.style.pointerEvents = "none"; const drawing = activeTool !== "cursor";
const target = document.elementFromPoint(ev.clientX, ev.clientY); canvasEl.classList.toggle("is-drawing", drawing);
if (target && target !== canvasEl) { canvasEl.style.pointerEvents = drawing ? "auto" : "none";
const opts = {
bubbles: true,
cancelable: true,
clientX: ev.clientX,
clientY: ev.clientY,
pointerId: ev.pointerId,
pointerType: ev.pointerType,
buttons: ev.buttons,
};
target.dispatchEvent(new PointerEvent(ev.type, opts));
}
const restore = function () {
if (activeTool === "cursor") canvasEl.style.pointerEvents = "auto";
window.removeEventListener("pointerup", restore, true);
};
window.addEventListener("pointerup", restore, true);
}
function onCursorPointerDown(ev) {
if (activeTool !== "cursor") return;
const loc = clientToLocal(ev);
for (let i = drawings.length - 1; i >= 0; i--) {
if (hitTestDrawing(drawings[i], loc.x, loc.y)) {
selectedId = drawings[i].id;
scheduleRedraw();
ev.preventDefault();
ev.stopPropagation();
return;
}
}
selectedId = null;
scheduleRedraw();
passthroughPointerToChart(ev);
} }
function setActiveTool(tool) { function setActiveTool(tool) {
@@ -639,12 +708,8 @@
btn.classList.toggle("is-active", btn.getAttribute("data-tool") === tool); btn.classList.toggle("is-active", btn.getAttribute("data-tool") === tool);
}); });
} }
const drawing = tool !== "cursor"; syncCanvasPointerMode();
if (canvasEl) { setChartInteraction(activeTool === "cursor");
canvasEl.classList.toggle("is-drawing", drawing);
canvasEl.style.pointerEvents = "auto";
}
setChartInteraction(!drawing);
scheduleRedraw(); scheduleRedraw();
} }
@@ -657,17 +722,15 @@
}); });
} }
let canvasBound = false;
function bindCanvas() { function bindCanvas() {
if (!canvasEl) return; if (!canvasEl || canvasBound) return;
canvasEl.addEventListener("pointerdown", function (ev) { canvasBound = true;
if (activeTool === "cursor") { canvasEl.addEventListener("pointerdown", onPointerDown);
onCursorPointerDown(ev);
return;
}
onPointerDown(ev);
});
canvasEl.addEventListener("pointermove", onPointerMove); canvasEl.addEventListener("pointermove", onPointerMove);
canvasEl.addEventListener("pointerup", onPointerUp); canvasEl.addEventListener("pointerup", onPointerUp);
canvasEl.addEventListener("pointercancel", onPointerUp);
canvasEl.addEventListener("dblclick", onDblClick); canvasEl.addEventListener("dblclick", onDblClick);
} }
@@ -678,6 +741,8 @@
mainEl = opts.mainEl || null; mainEl = opts.mainEl || null;
canvasEl = opts.canvasEl || null; canvasEl = opts.canvasEl || null;
toolbarEl = opts.toolbarEl || null; toolbarEl = opts.toolbarEl || null;
getCandlesFn = opts.getCandles || null;
mountCanvasOverlay();
bindToolbar(); bindToolbar();
bindCanvas(); bindCanvas();
setActiveTool("cursor"); setActiveTool("cursor");
+3 -3
View File
@@ -15,7 +15,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" /> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript> <noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260608-market-draw-v1" /> <link rel="stylesheet" href="/assets/app.css?v=20260608-market-draw-v2" />
</head> </head>
<body> <body>
<div class="app-bg" aria-hidden="true"></div> <div class="app-bg" aria-hidden="true"></div>
@@ -392,8 +392,8 @@
<div id="toast"></div> <div id="toast"></div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script> <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart_draw.js?v=20260608-market-draw-v1"></script> <script src="/assets/chart_draw.js?v=20260608-market-draw-v2"></script>
<script src="/assets/chart.js?v=20260608-market-draw-v1"></script> <script src="/assets/chart.js?v=20260608-market-draw-v2"></script>
<script src="/assets/archive.js?v=20260607-hub-archive-v6"></script> <script src="/assets/archive.js?v=20260607-hub-archive-v6"></script>
<script src="/assets/ai_review_render.js?v=2"></script> <script src="/assets/ai_review_render.js?v=2"></script>
<script src="/assets/app.js?v=20260607-hub-archive-v1"></script> <script src="/assets/app.js?v=20260607-hub-archive-v1"></script>