not bad
This commit is contained in:
parent
27ceb6568a
commit
4a8aff90b2
7 changed files with 3806 additions and 83 deletions
97
app.js
97
app.js
|
|
@ -70,6 +70,10 @@ const dragState = {
|
|||
startClientY: 0,
|
||||
startCameraX: 0,
|
||||
startCameraY: 0,
|
||||
lastClientX: 0,
|
||||
lastClientY: 0,
|
||||
panRemainderX: 0,
|
||||
panRemainderY: 0,
|
||||
selectStart: null,
|
||||
selectEnd: null,
|
||||
selectPath: null,
|
||||
|
|
@ -241,15 +245,34 @@ function selectionPathToShape(points) {
|
|||
};
|
||||
}
|
||||
|
||||
function syncSelectionSvgToCanvas() {
|
||||
if (!selectionSvgEl || !canvas) return null;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const width = Math.max(1, rect.width || canvas.clientWidth || canvas.width || 1);
|
||||
const height = Math.max(1, rect.height || canvas.clientHeight || canvas.height || 1);
|
||||
selectionSvgEl.style.left = `${canvas.offsetLeft}px`;
|
||||
selectionSvgEl.style.top = `${canvas.offsetTop}px`;
|
||||
selectionSvgEl.style.width = `${width}px`;
|
||||
selectionSvgEl.style.height = `${height}px`;
|
||||
selectionSvgEl.setAttribute("width", String(width));
|
||||
selectionSvgEl.setAttribute("height", String(height));
|
||||
selectionSvgEl.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
function drawSelectionSvg(points, invalid = false) {
|
||||
if (!selectionSvgEl) return;
|
||||
if (!points || points.length < 3) {
|
||||
const bounds = syncSelectionSvgToCanvas();
|
||||
if (!bounds || !points || points.length < 3) {
|
||||
selectionSvgEl.style.display = "none";
|
||||
selectionSvgEl.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
const pts = points.map((p) => `${p.x},${p.y}`).join(" ");
|
||||
selectionSvgEl.setAttribute("viewBox", `0 0 ${canvas.clientWidth || canvas.width || 1} ${canvas.clientHeight || canvas.height || 1}`);
|
||||
const pts = points.map((p) => {
|
||||
const x = Math.min(Math.max(p.x, 0), bounds.width);
|
||||
const y = Math.min(Math.max(p.y, 0), bounds.height);
|
||||
return `${x},${y}`;
|
||||
}).join(" ");
|
||||
selectionSvgEl.innerHTML = `<polygon points="${pts}" />`;
|
||||
selectionSvgEl.style.display = "block";
|
||||
selectionSvgEl.classList.toggle("invalid", !!invalid);
|
||||
|
|
@ -375,6 +398,8 @@ function clearDragMode() {
|
|||
dragState.mode = null;
|
||||
dragState.pointerId = null;
|
||||
dragState.pendingCamera = null;
|
||||
dragState.panRemainderX = 0;
|
||||
dragState.panRemainderY = 0;
|
||||
if (dragState.panRaf != null) {
|
||||
cancelAnimationFrame(dragState.panRaf);
|
||||
dragState.panRaf = null;
|
||||
|
|
@ -392,7 +417,10 @@ function schedulePanRedraw(camera) {
|
|||
dragState.pendingCamera = null;
|
||||
if (next.x === state.camera.x && next.y === state.camera.y) return;
|
||||
state.camera = next;
|
||||
redraw({ fastTerrain: true });
|
||||
// Do not auto-expand the backing world while a pointer drag is active.
|
||||
// Expansion shifts world coordinates; doing it mid-drag invalidates the
|
||||
// pointer-to-camera baseline and can make the viewport appear to jump.
|
||||
redraw({ fastTerrain: true, allowWorldExpand: false });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -432,6 +460,10 @@ function handleMapPointerDown(event) {
|
|||
dragState.startClientY = event.clientY;
|
||||
dragState.startCameraX = state.camera.x;
|
||||
dragState.startCameraY = state.camera.y;
|
||||
dragState.lastClientX = event.clientX;
|
||||
dragState.lastClientY = event.clientY;
|
||||
dragState.panRemainderX = 0;
|
||||
dragState.panRemainderY = 0;
|
||||
tooltipEl?.classList.remove("visible");
|
||||
|
||||
if (event.button === 0) {
|
||||
|
|
@ -455,14 +487,34 @@ function handleMapPointerMove(event) {
|
|||
tooltipEl?.classList.remove("visible");
|
||||
|
||||
if (dragState.mode === "pan") {
|
||||
const cellSize = Math.max(1, displayedCellSize());
|
||||
const dxCells = Math.round((event.clientX - dragState.startClientX) / cellSize);
|
||||
const dyCells = Math.round((event.clientY - dragState.startClientY) / cellSize);
|
||||
const nextCamera = clampCameraForView({
|
||||
x: dragState.startCameraX - dxCells,
|
||||
y: dragState.startCameraY - dyCells,
|
||||
}, viewportSizeForZoom(state.zoom));
|
||||
schedulePanRedraw(nextCamera);
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const maxDeltaX = Math.max(320, rect.width * 0.72);
|
||||
const maxDeltaY = Math.max(240, rect.height * 0.72);
|
||||
const rawDx = event.clientX - dragState.lastClientX;
|
||||
const rawDy = event.clientY - dragState.lastClientY;
|
||||
dragState.lastClientX = event.clientX;
|
||||
dragState.lastClientY = event.clientY;
|
||||
|
||||
// Pointer capture can occasionally deliver a stale/outlier coordinate after
|
||||
// a tab switch, resize, context-menu gesture, or OS-level event hiccup. A
|
||||
// single implausibly large delta would otherwise become a large camera jump.
|
||||
if (Math.abs(rawDx) <= maxDeltaX && Math.abs(rawDy) <= maxDeltaY) {
|
||||
const cellSize = Math.max(1, displayedCellSize());
|
||||
const totalX = dragState.panRemainderX + rawDx / cellSize;
|
||||
const totalY = dragState.panRemainderY + rawDy / cellSize;
|
||||
const dxCells = totalX < 0 ? Math.ceil(totalX) : Math.floor(totalX);
|
||||
const dyCells = totalY < 0 ? Math.ceil(totalY) : Math.floor(totalY);
|
||||
dragState.panRemainderX = totalX - dxCells;
|
||||
dragState.panRemainderY = totalY - dyCells;
|
||||
if (dxCells || dyCells) {
|
||||
const base = dragState.pendingCamera || state.camera;
|
||||
const nextCamera = clampCameraForView({
|
||||
x: base.x - dxCells,
|
||||
y: base.y - dyCells,
|
||||
}, viewportSizeForZoom(state.zoom));
|
||||
schedulePanRedraw(nextCamera);
|
||||
}
|
||||
}
|
||||
} else if (dragState.mode === "select") {
|
||||
dragState.selectEnd = clampCanvasPoint(event);
|
||||
if (!dragState.selectPath || Math.hypot(dragState.selectEnd.x - dragState.selectPath[dragState.selectPath.length - 1].x, dragState.selectEnd.y - dragState.selectPath[dragState.selectPath.length - 1].y) >= 3) {
|
||||
|
|
@ -954,17 +1006,22 @@ function redraw(options = {}) {
|
|||
const viewSize = syncViewportSize();
|
||||
const expansion = options.allowWorldExpand === false ? null : ensureWorldPaddingForCamera(state.world, state.camera, viewSize.width, viewSize.height);
|
||||
if (expansion?.expanded) {
|
||||
state.camera = { x: (state.camera?.x || 0) + (expansion.dx || 0), y: (state.camera?.y || 0) + (expansion.dy || 0) };
|
||||
const ex = expansion.dx || 0;
|
||||
const ey = expansion.dy || 0;
|
||||
state.camera = { x: (state.camera?.x || 0) + ex, y: (state.camera?.y || 0) + ey };
|
||||
if (dragState.mode === "pan") {
|
||||
dragState.startCameraX += ex;
|
||||
dragState.startCameraY += ey;
|
||||
if (dragState.pendingCamera) dragState.pendingCamera = { x: dragState.pendingCamera.x + ex, y: dragState.pendingCamera.y + ey };
|
||||
}
|
||||
if (state.selectionRect) {
|
||||
const dx = expansion.dx || 0;
|
||||
const dy = expansion.dy || 0;
|
||||
state.selectionRect = {
|
||||
...state.selectionRect,
|
||||
x0: state.selectionRect.x0 + dx,
|
||||
y0: state.selectionRect.y0 + dy,
|
||||
x1: state.selectionRect.x1 + dx,
|
||||
y1: state.selectionRect.y1 + dy,
|
||||
polygon: Array.isArray(state.selectionRect.polygon) ? state.selectionRect.polygon.map((p) => ({ x: p.x + dx, y: p.y + dy })) : state.selectionRect.polygon,
|
||||
x0: state.selectionRect.x0 + ex,
|
||||
y0: state.selectionRect.y0 + ey,
|
||||
x1: state.selectionRect.x1 + ex,
|
||||
y1: state.selectionRect.y1 + ey,
|
||||
polygon: Array.isArray(state.selectionRect.polygon) ? state.selectionRect.polygon.map((p) => ({ x: p.x + ex, y: p.y + ey })) : state.selectionRect.polygon,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue