feat: Add spatial workspace architecture design document and refactor card drag logic to use new LayoutCard drag state management.

This commit is contained in:
2025-11-26 17:00:56 +01:00
parent 81194032e0
commit 155d8c5c4a
3 changed files with 80 additions and 23 deletions
+27 -12
View File
@@ -1,24 +1,39 @@
import { LayoutStore } from './LayoutSystem';
const capturedCards = new Set<string>();
export const handleDragStart = (store: LayoutStore, selection: string[], leadingId: string, offset: { x: number, y: number }) => {
const leadingCard = store.items.get(leadingId);
if (!leadingCard) return;
selection.forEach(id => {
const card = store.items.get(id);
if (!card) return;
let myOffset = offset;
if (id !== leadingId) {
myOffset = {
x: offset.x + (leadingCard.x - card.x),
y: offset.y + (leadingCard.y - card.y)
};
}
card.beginDrag(myOffset);
});
};
export const handleDragMove = (store: LayoutStore, selection: string[], delta: { x: number, y: number }) => {
selection.forEach(id => {
const card = store.items.get(id);
if (card) {
if (!capturedCards.has(id)) {
card.captureDragStart();
capturedCards.add(id);
}
card.update({
x: card.dragStartX + delta.x,
y: card.dragStartY + delta.y
});
card.continueDrag(delta);
}
});
};
export const handleDragEnd = () => {
capturedCards.clear();
export const handleDragEnd = (store: LayoutStore, selection: string[]) => {
selection.forEach(id => {
const card = store.items.get(id);
if (card) {
card.finishDrag();
}
});
};
+19 -1
View File
@@ -25,6 +25,7 @@ export class LayoutCard implements LayoutCardState {
public store: LayoutStore;
public dragStartX: number = 0;
public dragStartY: number = 0;
private dragOffset: { x: number, y: number } | null = null;
constructor(id: string, store: LayoutStore, initialData: Partial<LayoutCardState> = {}, ref: HTMLElement | null = null) {
this.id = id;
@@ -39,11 +40,28 @@ export class LayoutCard implements LayoutCardState {
this.applyTransform();
}
captureDragStart() {
beginDrag(offset: { x: number, y: number }) {
this.dragOffset = offset;
this.dragStartX = this.x;
this.dragStartY = this.y;
}
continueDrag(delta: { x: number, y: number }) {
if (!this.dragOffset) return;
const newX = this.x + delta.x;
const newY = this.y + delta.y;
this.update({
x: newX,
y: newY
});
}
finishDrag() {
this.dragOffset = null;
}
update(changes: Partial<LayoutCardState>) {
Object.assign(this, changes);
if (changes.width !== undefined || changes.height !== undefined) {
+34 -10
View File
@@ -1,7 +1,7 @@
import React, { useCallback, useRef } from 'react';
import { LayoutCard } from './LayoutSystem';
import { handleDragMove, handleDragEnd } from './CardDragLogic';
import { handleDragMove, handleDragEnd, handleDragStart } from './CardDragLogic';
const DRAG_THRESHOLD = 3;
@@ -17,6 +17,7 @@ export const useCardPointer = (
) => {
const [state, setState] = React.useState<PointerState>('idle');
const initialPosition = useRef<{ x: number, y: number } | null>(null);
const lastPosition = useRef<{ x: number, y: number } | null>(null);
const updateState = useCallback((e: React.PointerEvent) => {
if (state === 'click' && initialPosition.current) {
@@ -37,6 +38,7 @@ export const useCardPointer = (
(e.target as Element).setPointerCapture(e.pointerId);
setState('click');
initialPosition.current = { x: e.clientX, y: e.clientY };
lastPosition.current = { x: e.clientX, y: e.clientY };
}, []);
const onPointerMove = useCallback((e: React.PointerEvent) => {
@@ -44,24 +46,45 @@ export const useCardPointer = (
updateState(e);
if (state === 'drag-start') {
let effectiveSelection = selection;
if (!isSelected) {
onSelect([card.id], hasModifier);
if (!hasModifier) {
card.bringToFront();
if (hasModifier) {
effectiveSelection = [...selection, card.id];
} else {
effectiveSelection = [card.id];
}
}
card.bringToFront();
setState('drag');
if (initialPosition.current) {
const rect = card.ref.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const offset = {
x: initialPosition.current.x - centerX,
y: initialPosition.current.y - centerY
};
handleDragStart(card.store, effectiveSelection, card.id, offset);
// Reset lastPosition to current pointer to avoid jump on first move
lastPosition.current = { x: e.clientX, y: e.clientY };
}
}
if (state === 'drag' && initialPosition.current) {
if (state === 'drag' && lastPosition.current) {
const delta = {
x: e.clientX - initialPosition.current.x,
y: e.clientY - initialPosition.current.y
x: e.clientX - lastPosition.current.x,
y: e.clientY - lastPosition.current.y
};
handleDragMove(card.store, selection, delta);
if (delta.x !== 0 || delta.y !== 0) {
handleDragMove(card.store, selection, delta);
lastPosition.current = { x: e.clientX, y: e.clientY };
}
}
}, [card, state, updateState, isSelected, selection, onSelect]);
@@ -70,7 +93,7 @@ export const useCardPointer = (
updateState(e);
if (state === 'drag' || state === 'drag-start') {
handleDragEnd();
handleDragEnd(card.store, selection);
} else if (state === 'click') {
if (isSelected) {
const isUnobstructed = card.isUnobstructed();
@@ -91,8 +114,9 @@ export const useCardPointer = (
setState('idle');
initialPosition.current = null;
lastPosition.current = null;
(e.target as Element).releasePointerCapture(e.pointerId);
}, [card, state, isSelected, onSelect, onDeselect, onDocumentActivate, updateState]);
}, [card, state, isSelected, onSelect, onDeselect, onDocumentActivate, updateState, selection]);
return {
onPointerDown,