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; 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) { handleEvent(event: PointerEvent | KeyboardEvent | TouchEvent) {
// Handle touch events for mobile drag support // Handle touch events for mobile drag support
if (event instanceof TouchEvent) { if (event instanceof TouchEvent) {
@ -454,7 +466,7 @@ export class FolkShape extends FolkElement {
x: this.#rect.width * this.#rect.rotateOrigin.x, x: this.#rect.width * this.#rect.rotateOrigin.x,
y: this.#rect.height * this.#rect.rotateOrigin.y, 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; this.#startAngle = Vector.angleFromOrigin(mousePos, parentRotateOrigin) - this.#rect.rotation;
} }
@ -526,7 +538,7 @@ export class FolkShape extends FolkElement {
const mousePos = const mousePos =
event instanceof KeyboardEvent event instanceof KeyboardEvent
? { x: currentPos.x + moveDelta.x, y: currentPos.y + moveDelta.y } ? { x: currentPos.x + moveDelta.x, y: currentPos.y + moveDelta.y }
: { x: event.clientX, y: event.clientY }; : this.#screenToParent(event.clientX, event.clientY);
this.#handleResize( this.#handleResize(
handle as ResizeHandle, handle as ResizeHandle,
@ -544,7 +556,7 @@ export class FolkShape extends FolkElement {
y: this.#rect.height * this.#rect.rotateOrigin.y, y: this.#rect.height * this.#rect.rotateOrigin.y,
}); });
const currentAngle = Vector.angleFromOrigin( const currentAngle = Vector.angleFromOrigin(
{ x: event.clientX, y: event.clientY }, this.#screenToParent(event.clientX, event.clientY),
parentRotateOrigin, parentRotateOrigin,
); );
this.rotation = currentAngle - this.#startAngle; this.rotation = currentAngle - this.#startAngle;

View File

@ -2664,6 +2664,12 @@
canvas.style.backgroundPosition = `${panX - 1}px ${panY - 1}px`; canvas.style.backgroundPosition = `${panX - 1}px ${panY - 1}px`;
// Keep MI bridge in sync // Keep MI bridge in sync
__miCanvasBridge.setViewport(panX, panY, scale); __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", () => { document.getElementById("zoom-in").addEventListener("click", () => {