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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-27 14:07:10 -08:00
parent 0555b5fa7f
commit 15e6a9b9ba
1 changed files with 51 additions and 1 deletions

View File

@ -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()