feat: Add physics model design document, implement multi-card snapping and layout system improvements, and refine pointer event handling.

This commit is contained in:
2025-11-26 17:30:46 +01:00
parent 155d8c5c4a
commit 835dcc4faa
4 changed files with 69 additions and 4 deletions
+21 -2
View File
@@ -4,15 +4,34 @@ export const handleDragStart = (store: LayoutStore, selection: string[], leading
const leadingCard = store.items.get(leadingId);
if (!leadingCard) return;
// Snap all non-leading cards to the leading card's position
selection.forEach(id => {
if (id === leadingId) return;
const card = store.items.get(id);
if (card) {
const leadingCenterX = leadingCard.x + leadingCard.width / 2;
const leadingCenterY = leadingCard.y + leadingCard.height / 2;
const targetX = leadingCenterX - card.width / 2;
const targetY = leadingCenterY - card.height / 2;
card.snapTo(targetX, targetY);
}
});
selection.forEach(id => {
const card = store.items.get(id);
if (!card) return;
let myOffset = offset;
if (id !== leadingId) {
const pointerX = leadingCard.x + offset.x;
const pointerY = leadingCard.y + offset.y;
myOffset = {
x: offset.x + (leadingCard.x - card.x),
y: offset.y + (leadingCard.y - card.y)
x: pointerX - card.x,
y: pointerY - card.y
};
}
+38 -2
View File
@@ -179,6 +179,27 @@ export class LayoutCard implements LayoutCardState {
});
}
snapTo(x: number, y: number) {
this.update({ x, y });
if (!this.ref)
return;
this.ref.classList.add('desk-item--swoop');
this.ref.style.transform = `translate3d(${this.x}px, ${this.y}px, 0) rotate(${this.rotation}deg)`;
const cleanup = () => {
if (this.ref) {
this.ref.classList.remove('desk-item--swoop');
}
};
this.ref.addEventListener('transitionend', cleanup, { once: true });
// Safety timeout in case transitionend doesn't fire (e.g. element removed)
setTimeout(cleanup, 350);
}
toSnapshot(): LayoutCardState {
return {
id: this.id,
@@ -252,14 +273,29 @@ export class LayoutStore {
for (const card of this.items.values()) {
const dx = card.x - x;
const dy = card.y - y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < radius + card.outerRadius) {
// Check if center is within inner radius of top card
// Use squared distance to avoid sqrt
const distSq = dx * dx + dy * dy;
if (distSq < radius * radius) {
result.push(card);
}
}
return result;
}
getStackBelow(topCard: LayoutCard): string[] {
const candidates = this.getCardsInCircle(topCard.x, topCard.y, topCard.innerRadius);
return candidates
.filter(other => {
if (other.id === topCard.id) return false;
if (other.z >= topCard.z) return false;
return true;
})
.map(c => c.id);
}
getSnapshot() {
return Array.from(this.items.values()).map(card => card.toSnapshot());
}
+6
View File
@@ -35,6 +35,8 @@ export const useCardPointer = (
// Only left click
if (e.button !== 0) return;
e.preventDefault();
(e.target as Element).setPointerCapture(e.pointerId);
setState('click');
initialPosition.current = { x: e.clientX, y: e.clientY };
@@ -42,6 +44,8 @@ export const useCardPointer = (
}, []);
const onPointerMove = useCallback((e: React.PointerEvent) => {
e.preventDefault();
const hasModifier = e.metaKey || e.ctrlKey || e.shiftKey;
updateState(e);
@@ -89,6 +93,8 @@ export const useCardPointer = (
}, [card, state, updateState, isSelected, selection, onSelect]);
const onPointerUp = useCallback((e: React.PointerEvent) => {
e.preventDefault();
const hasModifier = e.metaKey || e.ctrlKey || e.shiftKey;
updateState(e);
@@ -15,6 +15,10 @@
overflow: hidden;
}
.desk-item--swoop {
transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.desk-item__card img {
width: 100%;
height: 100%;