fix: shape resize/rotate by converting screen coords to canvas space

Resize and rotation handlers in folk-shape.ts passed raw event.clientX/Y
(screen coordinates) to toLocalSpace/angleFromOrigin, but those methods
expect canvas-parent coordinates. With any zoom/pan, the two coordinate
systems diverge, making resize non-functional and rotation erratic.

Added #screenToParent() to convert viewport coords to the parent's
coordinate space using getBoundingClientRect + parent scale. Applied to:
- Resize handle drag (pointermove → toLocalSpace)
- Rotation start (pointerdown → angleFromOrigin)
- Rotation drag (pointermove → angleFromOrigin)

Also syncs ghost placeholder size with zoom changes so the dotted
preview stays accurate if user zooms while in placement mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-01 10:38:06 -08:00
parent 7616fe0757
commit aeb9247f96
2 changed files with 21 additions and 3 deletions

View File

@ -375,6 +375,18 @@ export class FolkShape extends FolkElement {
return rect.width / w;
}
/** Convert screen (viewport) coordinates to the parent's coordinate space. */
#screenToParent(screenX: number, screenY: number): Point {
const parent = this.parentElement;
if (!parent) return { x: screenX, y: screenY };
const parentRect = parent.getBoundingClientRect();
const zoom = this.#getParentScale();
return {
x: (screenX - parentRect.left) / zoom,
y: (screenY - parentRect.top) / zoom,
};
}
handleEvent(event: PointerEvent | KeyboardEvent | TouchEvent) {
// Handle touch events for mobile drag support
if (event instanceof TouchEvent) {
@ -454,7 +466,7 @@ export class FolkShape extends FolkElement {
x: this.#rect.width * this.#rect.rotateOrigin.x,
y: this.#rect.height * this.#rect.rotateOrigin.y,
});
const mousePos = { x: event.clientX, y: event.clientY };
const mousePos = this.#screenToParent(event.clientX, event.clientY);
this.#startAngle = Vector.angleFromOrigin(mousePos, parentRotateOrigin) - this.#rect.rotation;
}
@ -526,7 +538,7 @@ export class FolkShape extends FolkElement {
const mousePos =
event instanceof KeyboardEvent
? { x: currentPos.x + moveDelta.x, y: currentPos.y + moveDelta.y }
: { x: event.clientX, y: event.clientY };
: this.#screenToParent(event.clientX, event.clientY);
this.#handleResize(
handle as ResizeHandle,
@ -544,7 +556,7 @@ export class FolkShape extends FolkElement {
y: this.#rect.height * this.#rect.rotateOrigin.y,
});
const currentAngle = Vector.angleFromOrigin(
{ x: event.clientX, y: event.clientY },
this.#screenToParent(event.clientX, event.clientY),
parentRotateOrigin,
);
this.rotation = currentAngle - this.#startAngle;

View File

@ -2664,6 +2664,12 @@
canvas.style.backgroundPosition = `${panX - 1}px ${panY - 1}px`;
// Keep MI bridge in sync
__miCanvasBridge.setViewport(panX, panY, scale);
// Keep ghost placeholder size in sync with zoom
if (ghostEl && pendingTool) {
const defaults = SHAPE_DEFAULTS[pendingTool.tagName] || { width: 300, height: 200 };
ghostEl.style.width = (defaults.width * scale) + "px";
ghostEl.style.height = (defaults.height * scale) + "px";
}
}
document.getElementById("zoom-in").addEventListener("click", () => {