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;
height: 100%;
position: relative;
overflow: hidden;
}
.market-draw-canvas {
position: absolute;
inset: 0;
z-index: 3;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 20;
pointer-events: none;
touch-action: none;
}
.market-draw-canvas.is-drawing {
cursor: crosshair;
pointer-events: auto;
}
.market-exchange-badge {
+3
View File
@@ -247,6 +247,9 @@
mainEl: elChartMain,
canvasEl: elDrawCanvas,
toolbarEl: elDrawToolbar,
getCandles: function () {
return lastCandles;
},
});
window.HubChartDraw.setViewKey(currentChartViewKey());
drawAttached = true;
+136 -71
View File
@@ -35,6 +35,8 @@
let selectedId = null;
let redrawRaf = 0;
let unsubRange = null;
let getCandlesFn = null;
let brushPointerId = null;
function uid() {
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);
}
function getCandles() {
if (typeof getCandlesFn === "function") {
const rows = getCandlesFn();
return Array.isArray(rows) ? rows : [];
}
return [];
}
function timeToX(time) {
if (!chart || time == null) return null;
try {
return chart.timeScale().timeToCoordinate(time);
const x = chart.timeScale().timeToCoordinate(time);
return x == null || !Number.isFinite(x) ? null : x;
} catch (_) {
return null;
}
@@ -105,29 +116,87 @@
function priceToY(price) {
if (!series || price == null || !Number.isFinite(Number(price))) return null;
try {
return series.priceToCoordinate(Number(price));
const y = series.priceToCoordinate(Number(price));
return y == null || !Number.isFinite(y) ? null : y;
} catch (_) {
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) {
if (!chart || !series) return null;
let time = null;
let price = null;
try {
time = chart.timeScale().coordinateToTime(x);
price = series.coordinateToPrice(y);
} catch (_) {}
if (time == null || price == null || !Number.isFinite(Number(price))) return null;
return { time: time, price: Number(price) };
const time = xToTime(x);
const price = yToPrice(y);
if (time == null || price == null || !Number.isFinite(price)) return null;
return { time: time, price: price };
}
function clientToLocal(ev) {
const rect = canvasEl.getBoundingClientRect();
const rect = (hostEl || canvasEl).getBoundingClientRect();
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() {
if (redrawRaf) cancelAnimationFrame(redrawRaf);
redrawRaf = requestAnimationFrame(function () {
@@ -415,13 +484,36 @@
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) {
if (activeTool === "cursor" || activeTool === "clear" || activeTool === "erase") return;
if (!chart || !series) return;
if (activeTool === "cursor" || activeTool === "clear") return;
if (!chart || !series || !canvasEl) return;
const loc = clientToLocal(ev);
if (activeTool === "erase") {
tryEraseAt(loc.x, loc.y);
ev.preventDefault();
return;
}
const pt = xyToPoint(loc.x, loc.y);
if (!pt) return;
ev.preventDefault();
ev.stopPropagation();
try {
canvasEl.setPointerCapture(ev.pointerId);
brushPointerId = ev.pointerId;
} catch (_) {}
if (activeTool === "brush") {
draft = { type: "brush", points: [pt] };
@@ -467,22 +559,26 @@
}
function onPointerMove(ev) {
if (!draft) return;
const loc = clientToLocal(ev);
const pt = xyToPoint(loc.x, loc.y);
if (!pt) return;
if (activeTool === "brush" && draft.type === "brush") {
if (activeTool === "brush" && draft && draft.type === "brush") {
const loc = clientToLocal(ev);
const pt = xyToPoint(loc.x, loc.y);
if (!pt) return;
const last = draft.points[draft.points.length - 1];
const lx = timeToX(last.time);
const ly = priceToY(last.price);
const cx = timeToX(pt.time);
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);
scheduleRedraw();
}
ev.preventDefault();
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) {
draft.points[draft.points.length] = pt;
if (draft.points.length > pointsNeeded(activeTool)) {
@@ -493,6 +589,12 @@
}
function onPointerUp(ev) {
if (brushPointerId != null) {
try {
canvasEl.releasePointerCapture(brushPointerId);
} catch (_) {}
brushPointerId = null;
}
if (activeTool === "brush" && draft && draft.type === "brush" && draft.points.length > 1) {
commitDrawing(draft);
}
@@ -569,44 +671,11 @@
}
}
function passthroughPointerToChart(ev) {
if (!hostEl) return;
canvasEl.style.pointerEvents = "none";
const target = document.elementFromPoint(ev.clientX, ev.clientY);
if (target && target !== canvasEl) {
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 syncCanvasPointerMode() {
if (!canvasEl) return;
const drawing = activeTool !== "cursor";
canvasEl.classList.toggle("is-drawing", drawing);
canvasEl.style.pointerEvents = drawing ? "auto" : "none";
}
function setActiveTool(tool) {
@@ -639,12 +708,8 @@
btn.classList.toggle("is-active", btn.getAttribute("data-tool") === tool);
});
}
const drawing = tool !== "cursor";
if (canvasEl) {
canvasEl.classList.toggle("is-drawing", drawing);
canvasEl.style.pointerEvents = "auto";
}
setChartInteraction(!drawing);
syncCanvasPointerMode();
setChartInteraction(activeTool === "cursor");
scheduleRedraw();
}
@@ -657,17 +722,15 @@
});
}
let canvasBound = false;
function bindCanvas() {
if (!canvasEl) return;
canvasEl.addEventListener("pointerdown", function (ev) {
if (activeTool === "cursor") {
onCursorPointerDown(ev);
return;
}
onPointerDown(ev);
});
if (!canvasEl || canvasBound) return;
canvasBound = true;
canvasEl.addEventListener("pointerdown", onPointerDown);
canvasEl.addEventListener("pointermove", onPointerMove);
canvasEl.addEventListener("pointerup", onPointerUp);
canvasEl.addEventListener("pointercancel", onPointerUp);
canvasEl.addEventListener("dblclick", onDblClick);
}
@@ -678,6 +741,8 @@
mainEl = opts.mainEl || null;
canvasEl = opts.canvasEl || null;
toolbarEl = opts.toolbarEl || null;
getCandlesFn = opts.getCandles || null;
mountCanvasOverlay();
bindToolbar();
bindCanvas();
setActiveTool("cursor");
+3 -3
View File
@@ -15,7 +15,7 @@
<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'" />
<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>
<body>
<div class="app-bg" aria-hidden="true"></div>
@@ -392,8 +392,8 @@
<div id="toast"></div>
<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.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-v2"></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/app.js?v=20260607-hub-archive-v1"></script>