fix: push overlapping siblings instead of displacing the dragged shape
The overlap resolver now moves siblings in the drag direction rather than snapping the dragged shape away from them. Supports chain-pushing (A pushes B into C) with a recursion depth of 3. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7db6068229
commit
658eb966d6
|
|
@ -725,19 +725,29 @@ export class FolkShape extends FolkElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After moving, push this shape away from any overlapping siblings.
|
* After moving, push overlapping siblings out of the way.
|
||||||
* Uses minimum penetration depth — picks the smallest displacement
|
* The dragged shape stays where the cursor placed it;
|
||||||
* among all four directions to resolve each overlap.
|
* siblings are displaced in the drag direction.
|
||||||
*/
|
*/
|
||||||
#resolveOverlaps(_dx: number, _dy: number) {
|
#resolveOverlaps(dx: number, dy: number) {
|
||||||
const parent = this.parentElement;
|
const parent = this.parentElement;
|
||||||
if (!parent) return;
|
if (!parent) return;
|
||||||
|
this.#pushSiblings(parent, dx, dy, new Set<FolkShape>([this]), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For each sibling overlapping `pusher`, move the sibling out of the way.
|
||||||
|
* Recurses so chain-pushing works (A pushes B into C → C also moves).
|
||||||
|
*/
|
||||||
|
#pushSiblings(parent: Element, dx: number, dy: number, excluded: Set<FolkShape>, depth: number) {
|
||||||
|
if (depth <= 0) return;
|
||||||
|
|
||||||
const gap = FolkShape.GAP;
|
const gap = FolkShape.GAP;
|
||||||
const me = { x: this.x, y: this.y, w: this.width, h: this.height };
|
const me = { x: this.x, y: this.y, w: this.width, h: this.height };
|
||||||
|
|
||||||
for (const sibling of parent.children) {
|
for (const sibling of parent.children) {
|
||||||
if (sibling === this || !(sibling instanceof FolkShape)) continue;
|
if (!(sibling instanceof FolkShape)) continue;
|
||||||
|
if (excluded.has(sibling)) continue;
|
||||||
if (sibling.tagName.toLowerCase() === "folk-arrow") continue;
|
if (sibling.tagName.toLowerCase() === "folk-arrow") continue;
|
||||||
|
|
||||||
const other = { x: sibling.x, y: sibling.y, w: sibling.width, h: sibling.height };
|
const other = { x: sibling.x, y: sibling.y, w: sibling.width, h: sibling.height };
|
||||||
|
|
@ -745,32 +755,40 @@ export class FolkShape extends FolkElement {
|
||||||
// Check overlap (with gap buffer)
|
// Check overlap (with gap buffer)
|
||||||
const overlapX = me.x < other.x + other.w + gap && me.x + me.w + gap > other.x;
|
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;
|
const overlapY = me.y < other.y + other.h + gap && me.y + me.h + gap > other.y;
|
||||||
|
|
||||||
if (!overlapX || !overlapY) continue;
|
if (!overlapX || !overlapY) continue;
|
||||||
|
|
||||||
// Distance to clear on each side (4 possible escape directions)
|
// How far to push the sibling in each direction to clear the overlap
|
||||||
const clearRight = (other.x + other.w + gap) - me.x;
|
const pushRight = (me.x + me.w + gap) - other.x; // sibling moves +x
|
||||||
const clearLeft = other.x - (me.x + me.w + gap);
|
const pushLeft = (me.x) - (other.x + other.w + gap); // sibling moves -x
|
||||||
const clearDown = (other.y + other.h + gap) - me.y;
|
const pushDown = (me.y + me.h + gap) - other.y; // sibling moves +y
|
||||||
const clearUp = other.y - (me.y + me.h + gap);
|
const pushUp = (me.y) - (other.y + other.h + gap); // sibling moves -y
|
||||||
|
|
||||||
// Pick the direction with smallest absolute displacement
|
|
||||||
const candidates = [
|
const candidates = [
|
||||||
{ axis: "x" as const, d: clearRight },
|
{ axis: "x" as const, d: pushRight },
|
||||||
{ axis: "x" as const, d: clearLeft },
|
{ axis: "x" as const, d: pushLeft },
|
||||||
{ axis: "y" as const, d: clearDown },
|
{ axis: "y" as const, d: pushDown },
|
||||||
{ axis: "y" as const, d: clearUp },
|
{ axis: "y" as const, d: pushUp },
|
||||||
];
|
];
|
||||||
const best = candidates.reduce((a, b) => Math.abs(a.d) < Math.abs(b.d) ? a : b);
|
|
||||||
|
|
||||||
|
// Prefer directions aligned with drag movement, break ties by smallest displacement
|
||||||
|
const best = candidates.reduce((a, b) => {
|
||||||
|
const aAligned = (a.axis === "x" ? a.d * dx : a.d * dy) > 0;
|
||||||
|
const bAligned = (b.axis === "x" ? b.d * dx : b.d * dy) > 0;
|
||||||
|
if (aAligned !== bAligned) return aAligned ? a : b;
|
||||||
|
return Math.abs(a.d) < Math.abs(b.d) ? a : b;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Push the sibling (public setters trigger requestUpdate + transform events)
|
||||||
if (best.axis === "x") {
|
if (best.axis === "x") {
|
||||||
this.#rect.x += best.d;
|
sibling.x += best.d;
|
||||||
} else {
|
} else {
|
||||||
this.#rect.y += best.d;
|
sibling.y += best.d;
|
||||||
}
|
}
|
||||||
|
|
||||||
me.x = this.#rect.x;
|
// Recurse: the pushed sibling may now overlap others
|
||||||
me.y = this.#rect.y;
|
const nextExcluded = new Set(excluded);
|
||||||
|
nextExcluded.add(sibling);
|
||||||
|
sibling.#pushSiblings(parent, dx, dy, nextExcluded, depth - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue