pcb-retrace/docs/canvas-ui.js

212 lines
6.2 KiB
JavaScript
Raw Normal View History

/*
* Copyright (c) 2025-2026 Taras Greben
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
* See LICENSE file for details.
*/
/* canvas-ui.js - Shared Canvas Logic (Mobile/Touch Ready + Resize Hook) */
2025-12-21 00:31:26 +02:00
class PanZoomCanvas {
constructor(id, onDraw, onClick, onDragPt) {
this.canvas = document.getElementById(id);
this.container = this.canvas ? this.canvas.parentElement : null;
this.ctx = this.canvas ? this.canvas.getContext('2d') : null;
this.bmp = null;
this.t = {x:0, y:0, k:1};
2025-12-21 00:31:26 +02:00
this.lm = {x:0, y:0};
this.activePtIdx = -1;
this.isMirrored = false;
this.onDraw = onDraw;
this.onClick = onClick;
2025-12-21 00:31:26 +02:00
this.onDragPt = onDragPt;
this.onMouseMove = null;
this.onPointerDown = null;
// New: Resize Callback hook
this.onResize = null;
this.evCache = [];
this.prevDiff = -1;
this.isDragging = false;
this.totalDragDist = 0;
2025-12-21 00:31:26 +02:00
if(this.container) {
new ResizeObserver(() => {
if(this.container && this.container.clientWidth > 0) {
this.canvas.width = this.container.clientWidth;
this.canvas.height = this.container.clientHeight;
this.draw();
// Trigger Hook
if (this.onResize) this.onResize(this.canvas.width, this.canvas.height);
2025-12-21 00:31:26 +02:00
}
}).observe(this.container);
this.initEvents();
}
}
initEvents() {
this.canvas.style.touchAction = 'none';
this.canvas.addEventListener('wheel', e => {
2025-12-21 00:31:26 +02:00
e.preventDefault();
const f = Math.exp(-e.deltaY * 0.001);
this.zoomAt(e.clientX, e.clientY, f);
}, { passive: false });
2025-12-21 00:31:26 +02:00
this.canvas.addEventListener('pointerdown', e => this.handlePointerDown(e));
this.canvas.addEventListener('pointermove', e => this.handlePointerMove(e));
this.canvas.addEventListener('pointerup', e => this.handlePointerUp(e));
this.canvas.addEventListener('pointercancel', e => this.handlePointerUp(e));
this.canvas.addEventListener('pointerout', e => this.handlePointerUp(e));
this.canvas.addEventListener('pointerleave', e => this.handlePointerUp(e));
this.canvas.addEventListener('contextmenu', e => {
2025-12-21 00:31:26 +02:00
const coords = this.getImgCoords(e.clientX, e.clientY);
if (this.onDragPt) this.onDragPt(coords.x, coords.y, 'delete');
});
}
zoomAt(clientX, clientY, factor) {
const r = this.canvas.getBoundingClientRect();
const mx = clientX - r.left;
const my = clientY - r.top;
const wx = (mx - this.t.x) / this.t.k;
const wy = (my - this.t.y) / this.t.k;
this.t.k = Math.max(0.01, Math.min(20, this.t.k * factor));
this.t.x = mx - wx * this.t.k;
this.t.y = my - wy * this.t.k;
this.draw();
}
handlePointerDown(e) {
this.canvas.setPointerCapture(e.pointerId);
this.evCache.push(e);
this.totalDragDist = 0;
if (this.onPointerDown) this.onPointerDown(e);
const coords = this.getImgCoords(e.clientX, e.clientY);
if (this.onDragPt) {
const idx = this.onDragPt(coords.x, coords.y, 'check');
if (idx !== -1) {
this.activePtIdx = idx;
this.isDragging = true;
this.lm = { x: e.clientX, y: e.clientY };
return;
2025-12-21 00:31:26 +02:00
}
}
this.isDragging = true;
this.lm = { x: e.clientX, y: e.clientY };
this.activePtIdx = -1;
}
handlePointerMove(e) {
const index = this.evCache.findIndex(cached => cached.pointerId === e.pointerId);
if (index > -1) this.evCache[index] = e;
if (this.evCache.length === 2) {
const dx = this.evCache[0].clientX - this.evCache[1].clientX;
const dy = this.evCache[0].clientY - this.evCache[1].clientY;
const curDiff = Math.hypot(dx, dy);
if (this.prevDiff > 0) {
const factor = 1 + ((curDiff - this.prevDiff) * 0.01);
const cx = (this.evCache[0].clientX + this.evCache[1].clientX) / 2;
const cy = (this.evCache[0].clientY + this.evCache[1].clientY) / 2;
this.zoomAt(cx, cy, factor);
2025-12-21 00:31:26 +02:00
}
this.prevDiff = curDiff;
this.totalDragDist += 20;
return;
}
2025-12-21 00:31:26 +02:00
if (this.isDragging && this.evCache.length === 1) {
const dx = e.clientX - this.lm.x;
const dy = e.clientY - this.lm.y;
this.totalDragDist += Math.hypot(dx, dy);
if (this.activePtIdx !== -1) {
const curr = this.getImgCoords(e.clientX, e.clientY);
const prev = this.getImgCoords(this.lm.x, this.lm.y);
this.onDragPt(curr.x - prev.x, curr.y - prev.y, 'move', this.activePtIdx);
} else {
this.t.x += dx;
this.t.y += dy;
this.draw();
2025-12-21 00:31:26 +02:00
}
this.lm = { x: e.clientX, y: e.clientY };
}
if(this.onMouseMove) {
const coords = this.getImgCoords(e.clientX, e.clientY);
this.onMouseMove(coords.x, coords.y);
}
}
handlePointerUp(e) {
const index = this.evCache.findIndex(cached => cached.pointerId === e.pointerId);
if (index > -1) this.evCache.splice(index, 1);
if (this.evCache.length < 2) this.prevDiff = -1;
if (this.evCache.length === 0) {
if (this.totalDragDist < 20) {
if (this.activePtIdx === -1 && this.onClick) {
const coords = this.getImgCoords(e.clientX, e.clientY);
this.onClick(coords.x, coords.y, e);
}
}
this.isDragging = false;
2025-12-21 00:31:26 +02:00
this.activePtIdx = -1;
}
2025-12-21 00:31:26 +02:00
}
getImgCoords(screenX, screenY) {
const r = this.canvas.getBoundingClientRect();
const mx = (screenX - r.left - this.t.x) / this.t.k;
const my = (screenY - r.top - this.t.y) / this.t.k;
if (this.isMirrored && this.bmp) { return { x: this.bmp.width - mx, y: my }; }
return { x: mx, y: my };
}
setMirror(val) { this.isMirrored = val; this.draw(); }
setImage(b) { this.bmp = b; this.draw(); }
setDimmed(isDimmed) {
this.canvas.style.filter = isDimmed ? "brightness(0.4) grayscale(100%)" : "none";
2025-12-21 00:31:26 +02:00
}
fit() {
if(!this.bmp || !this.canvas) return;
const vw = this.canvas.width;
const vh = this.canvas.height;
if (vw === 0 || vh === 0) return;
const iw = this.bmp.width;
const ih = this.bmp.height;
const scale = Math.min(vw / iw, vh / ih);
const cx = (vw - iw * scale) / 2;
const cy = (vh - ih * scale) / 2;
this.t = { x: cx, y: cy, k: scale };
this.draw();
}
draw() {
if(!this.ctx) return;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.save();
this.ctx.translate(this.t.x, this.t.y);
this.ctx.scale(this.t.k, this.t.k);
if (this.bmp) {
this.ctx.save();
if (this.isMirrored) {
this.ctx.translate(this.bmp.width, 0);
this.ctx.scale(-1, 1);
}
this.ctx.drawImage(this.bmp, 0, 0);
this.ctx.restore();
}
if (this.onDraw) this.onDraw(this.ctx, this.t.k);
this.ctx.restore();
}
}