fix(canvas): absolute drag positioning + remove Move Here drop ghosts
Replace movementX/Y delta accumulation with absolute mouse-to-shape offset tracking for drift-free drag. Remove drop suggestion overlay system entirely. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e70b40df9a
commit
1cbee3e4d1
|
|
@ -234,6 +234,7 @@ export class FolkShape extends FolkElement {
|
|||
#startAngle = 0;
|
||||
#lastTouchPos: Point | null = null;
|
||||
#isTouchDragging = false;
|
||||
#dragOffset: Point | null = null;
|
||||
|
||||
get x() {
|
||||
return this.#rect.x;
|
||||
|
|
@ -606,6 +607,15 @@ export class FolkShape extends FolkElement {
|
|||
this.#startAngle = Vector.angleFromOrigin(mousePos, parentRotateOrigin) - this.#rect.rotation;
|
||||
}
|
||||
|
||||
// Capture offset from mouse to shape origin for absolute drag tracking
|
||||
if (!handle) {
|
||||
const mouseInParent = this.#screenToParent(event.clientX, event.clientY);
|
||||
this.#dragOffset = {
|
||||
x: mouseInParent.x - this.#rect.x,
|
||||
y: mouseInParent.y - this.#rect.y,
|
||||
};
|
||||
}
|
||||
|
||||
target.addEventListener("pointermove", this);
|
||||
target.addEventListener("lostpointercapture", this);
|
||||
target.setPointerCapture(event.pointerId);
|
||||
|
|
@ -616,6 +626,7 @@ export class FolkShape extends FolkElement {
|
|||
|
||||
if (event.type === "lostpointercapture") {
|
||||
this.#internals.states.delete(handle || "move");
|
||||
this.#dragOffset = null;
|
||||
target.removeEventListener("pointermove", this);
|
||||
target.removeEventListener("lostpointercapture", this);
|
||||
this.#updateCursors();
|
||||
|
|
@ -638,6 +649,17 @@ export class FolkShape extends FolkElement {
|
|||
};
|
||||
} else if (event.type === "pointermove") {
|
||||
if (!target) return;
|
||||
|
||||
// For shape body drag, use absolute positioning (no drift)
|
||||
if ((target === this || isDragHandle) && !handle && this.#dragOffset) {
|
||||
const mouseInParent = this.#screenToParent(event.clientX, event.clientY);
|
||||
this.x = mouseInParent.x - this.#dragOffset.x;
|
||||
this.y = mouseInParent.y - this.#dragOffset.y;
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// For resize/rotate handles, use delta-based movement
|
||||
const zoom = (window.visualViewport?.scale ?? 1) * this.#getParentScale();
|
||||
moveDelta = {
|
||||
x: event.movementX / zoom,
|
||||
|
|
|
|||
|
|
@ -4644,8 +4644,8 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
|
|||
let activeDragShape = null;
|
||||
let unsnapX = 0, unsnapY = 0;
|
||||
let snapCorrecting = false;
|
||||
let dropGhostEl = null;
|
||||
let dropGhostTimeout = null;
|
||||
|
||||
|
||||
|
||||
function getSnapTargets(excludeEl) {
|
||||
return [...canvasContent.children]
|
||||
|
|
@ -4768,8 +4768,9 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
|
|||
// Only snap during moves, not resize/rotate
|
||||
if (dx === 0 && dy === 0) return;
|
||||
|
||||
unsnapX += dx;
|
||||
unsnapY += dy;
|
||||
// Use the shape's current position as the raw (unsnapped) target
|
||||
unsnapX = cur.x;
|
||||
unsnapY = cur.y;
|
||||
|
||||
const targets = getSnapTargets(shape);
|
||||
const { snapX, snapY, guides } = computeSnaps(
|
||||
|
|
@ -4781,10 +4782,8 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
|
|||
|
||||
// Apply snap correction if position differs from where drag placed it
|
||||
if (Math.abs(finalX - cur.x) > 0.1 || Math.abs(finalY - cur.y) > 0.1) {
|
||||
// Modify the emitted rect so CSS shows snap position this frame
|
||||
cur.x = finalX;
|
||||
cur.y = finalY;
|
||||
// Update internal state (queues a correction update we'll skip)
|
||||
snapCorrecting = true;
|
||||
shape.x = finalX;
|
||||
shape.y = finalY;
|
||||
|
|
@ -4797,75 +4796,7 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
|
|||
}
|
||||
}, { capture: true });
|
||||
|
||||
// Drop suggestion helpers
|
||||
function clearDropGhost() {
|
||||
if (dropGhostEl) {
|
||||
dropGhostEl.remove();
|
||||
dropGhostEl = null;
|
||||
}
|
||||
if (dropGhostTimeout) {
|
||||
clearTimeout(dropGhostTimeout);
|
||||
dropGhostTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
function showDropGhost(shape, gx, gy) {
|
||||
clearDropGhost();
|
||||
const ghost = document.createElement("div");
|
||||
ghost.style.cssText = `
|
||||
position: absolute;
|
||||
left: ${gx}px; top: ${gy}px;
|
||||
width: ${shape.width}px; height: ${shape.height}px;
|
||||
border: 2px dashed ${SNAP_COLOR};
|
||||
border-radius: 8px;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font: 12px system-ui;
|
||||
color: ${SNAP_COLOR};
|
||||
background: rgba(20, 184, 166, 0.05);
|
||||
transition: opacity 0.3s;
|
||||
`;
|
||||
ghost.textContent = "Move here?";
|
||||
ghost.addEventListener("click", () => {
|
||||
shape.x = gx;
|
||||
shape.y = gy;
|
||||
clearDropGhost();
|
||||
});
|
||||
canvasContent.appendChild(ghost);
|
||||
dropGhostEl = ghost;
|
||||
|
||||
// Auto-fade after 3 seconds
|
||||
dropGhostTimeout = setTimeout(() => {
|
||||
if (dropGhostEl === ghost) {
|
||||
ghost.style.opacity = "0";
|
||||
setTimeout(() => { if (dropGhostEl === ghost) clearDropGhost(); }, 300);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
// Dismiss on any other canvas click
|
||||
const dismissHandler = (ev) => {
|
||||
if (ev.target !== ghost) {
|
||||
clearDropGhost();
|
||||
canvas.removeEventListener("pointerdown", dismissHandler, { capture: true });
|
||||
}
|
||||
};
|
||||
canvas.addEventListener("pointerdown", dismissHandler, { capture: true });
|
||||
}
|
||||
|
||||
function checkDropSuggestion(shape) {
|
||||
const shapeRect = { x: shape.x, y: shape.y, width: shape.width, height: shape.height };
|
||||
const existing = getExistingShapeRects(shape);
|
||||
const overlaps = existing.some(e => rectsOverlap(shapeRect, e, 0));
|
||||
if (!overlaps) return;
|
||||
|
||||
const center = { x: shape.x + shape.width / 2, y: shape.y + shape.height / 2 };
|
||||
const pos = findFreePosition(shape.width, shape.height, center.x, center.y, shape);
|
||||
showDropGhost(shape, pos.x, pos.y);
|
||||
}
|
||||
|
||||
// ── Helpers for converting SVG drawings into folk-shape elements ──
|
||||
|
||||
|
|
@ -7392,14 +7323,11 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
|
|||
unsnapX = shape.x;
|
||||
unsnapY = shape.y;
|
||||
snapCorrecting = false;
|
||||
clearDropGhost();
|
||||
}
|
||||
function onShapeMoveEnd() {
|
||||
if (!activeDragShape) return;
|
||||
const shape = activeDragShape;
|
||||
activeDragShape = null;
|
||||
clearSnapGuides();
|
||||
checkDropSuggestion(shape);
|
||||
}
|
||||
|
||||
rwPrev.addEventListener("click", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue