From 15e6a9b9ba0d36b3de079280acb3202ed85e4c0f Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 27 Feb 2026 14:07:10 -0800 Subject: [PATCH] fix: remove scrollbar arrows from shapes + add collision slide-off - Change inner div overflow from scroll to hidden, removing browser scrollbar arrows that appeared on every canvas shape - Add shape collision detection: shapes now slide off each other with an 8px gap instead of overlapping when dragged (pointer + touch) Co-Authored-By: Claude Opus 4.6 --- lib/folk-shape.ts | 52 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/lib/folk-shape.ts b/lib/folk-shape.ts index 8d2564f..bb7fcd2 100644 --- a/lib/folk-shape.ts +++ b/lib/folk-shape.ts @@ -72,7 +72,7 @@ const styles = css` div { height: 100%; width: 100%; - overflow: scroll; + overflow: hidden; pointer-events: none; } @@ -249,6 +249,8 @@ export class FolkShape extends FolkElement { this.requestUpdate("rotation"); } + static GAP = 8; // minimum gap between shapes + #highlighted = false; get highlighted() { return this.#highlighted; @@ -348,6 +350,7 @@ export class FolkShape extends FolkElement { // Apply movement this.#rect.x += moveDelta.x; this.#rect.y += moveDelta.y; + this.#resolveOverlaps(moveDelta.x, moveDelta.y); this.requestUpdate(); this.#dispatchTransformEvent(); } @@ -442,6 +445,7 @@ export class FolkShape extends FolkElement { } else { this.x += moveDelta.x; this.y += moveDelta.y; + this.#resolveOverlaps(moveDelta.x, moveDelta.y); } event.preventDefault(); return; @@ -603,6 +607,52 @@ export class FolkShape extends FolkElement { this.requestUpdate(); } + /** + * After moving, push this shape away from any overlapping siblings. + * Uses the direction of the move to decide which side to slide to. + */ + #resolveOverlaps(dx: number, dy: number) { + const parent = this.parentElement; + if (!parent) return; + + const gap = FolkShape.GAP; + const me = { x: this.x, y: this.y, w: this.width, h: this.height }; + + for (const sibling of parent.children) { + if (sibling === this || !(sibling instanceof FolkShape)) continue; + if (sibling.tagName.toLowerCase() === "folk-arrow") continue; + + const other = { x: sibling.x, y: sibling.y, w: sibling.width, h: sibling.height }; + + // Check overlap (axis-aligned) + const overlapX = me.x < other.x + other.w + gap && me.x + me.w + gap > other.x; + const overlapY = me.y < other.y + other.h + gap && me.y + me.h + gap > other.y; + + if (!overlapX || !overlapY) continue; + + // Compute penetration depths from each side + const pushRight = (other.x + other.w + gap) - me.x; + const pushLeft = me.x + me.w + gap - other.x; + const pushDown = (other.y + other.h + gap) - me.y; + const pushUp = me.y + me.h + gap - other.y; + + // Pick the axis with the smallest penetration, biased by move direction + const minX = pushRight < pushLeft ? -pushRight : pushLeft; + const minY = pushDown < pushUp ? -pushDown : pushUp; + + if (Math.abs(minX) < Math.abs(minY)) { + // Slide horizontally + this.#rect.x += dx <= 0 ? -pushLeft : pushRight; + } else { + // Slide vertically + this.#rect.y += dy <= 0 ? -pushUp : pushDown; + } + + me.x = this.#rect.x; + me.y = this.#rect.y; + } + } + /** * Serialize shape to JSON for Automerge sync * Subclasses should override and call super.toJSON()