fix: market drawing tools overlay alignment and pointer handling
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user