feat: Add spatial workspace architecture design document and refactor card drag logic to use new LayoutCard drag state management.
This commit is contained in:
@@ -1,24 +1,39 @@
|
|||||||
import { LayoutStore } from './LayoutSystem';
|
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 }) => {
|
export const handleDragMove = (store: LayoutStore, selection: string[], delta: { x: number, y: number }) => {
|
||||||
selection.forEach(id => {
|
selection.forEach(id => {
|
||||||
const card = store.items.get(id);
|
const card = store.items.get(id);
|
||||||
if (card) {
|
if (card) {
|
||||||
if (!capturedCards.has(id)) {
|
card.continueDrag(delta);
|
||||||
card.captureDragStart();
|
|
||||||
capturedCards.add(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
card.update({
|
|
||||||
x: card.dragStartX + delta.x,
|
|
||||||
y: card.dragStartY + delta.y
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleDragEnd = () => {
|
export const handleDragEnd = (store: LayoutStore, selection: string[]) => {
|
||||||
capturedCards.clear();
|
selection.forEach(id => {
|
||||||
|
const card = store.items.get(id);
|
||||||
|
if (card) {
|
||||||
|
card.finishDrag();
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export class LayoutCard implements LayoutCardState {
|
|||||||
public store: LayoutStore;
|
public store: LayoutStore;
|
||||||
public dragStartX: number = 0;
|
public dragStartX: number = 0;
|
||||||
public dragStartY: 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) {
|
constructor(id: string, store: LayoutStore, initialData: Partial<LayoutCardState> = {}, ref: HTMLElement | null = null) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@@ -39,11 +40,28 @@ export class LayoutCard implements LayoutCardState {
|
|||||||
this.applyTransform();
|
this.applyTransform();
|
||||||
}
|
}
|
||||||
|
|
||||||
captureDragStart() {
|
beginDrag(offset: { x: number, y: number }) {
|
||||||
|
this.dragOffset = offset;
|
||||||
this.dragStartX = this.x;
|
this.dragStartX = this.x;
|
||||||
this.dragStartY = this.y;
|
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>) {
|
update(changes: Partial<LayoutCardState>) {
|
||||||
Object.assign(this, changes);
|
Object.assign(this, changes);
|
||||||
if (changes.width !== undefined || changes.height !== undefined) {
|
if (changes.width !== undefined || changes.height !== undefined) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useRef } from 'react';
|
import React, { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
import { LayoutCard } from './LayoutSystem';
|
import { LayoutCard } from './LayoutSystem';
|
||||||
import { handleDragMove, handleDragEnd } from './CardDragLogic';
|
import { handleDragMove, handleDragEnd, handleDragStart } from './CardDragLogic';
|
||||||
|
|
||||||
const DRAG_THRESHOLD = 3;
|
const DRAG_THRESHOLD = 3;
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ export const useCardPointer = (
|
|||||||
) => {
|
) => {
|
||||||
const [state, setState] = React.useState<PointerState>('idle');
|
const [state, setState] = React.useState<PointerState>('idle');
|
||||||
const initialPosition = useRef<{ x: number, y: number } | null>(null);
|
const initialPosition = useRef<{ x: number, y: number } | null>(null);
|
||||||
|
const lastPosition = useRef<{ x: number, y: number } | null>(null);
|
||||||
|
|
||||||
const updateState = useCallback((e: React.PointerEvent) => {
|
const updateState = useCallback((e: React.PointerEvent) => {
|
||||||
if (state === 'click' && initialPosition.current) {
|
if (state === 'click' && initialPosition.current) {
|
||||||
@@ -37,6 +38,7 @@ export const useCardPointer = (
|
|||||||
(e.target as Element).setPointerCapture(e.pointerId);
|
(e.target as Element).setPointerCapture(e.pointerId);
|
||||||
setState('click');
|
setState('click');
|
||||||
initialPosition.current = { x: e.clientX, y: e.clientY };
|
initialPosition.current = { x: e.clientX, y: e.clientY };
|
||||||
|
lastPosition.current = { x: e.clientX, y: e.clientY };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onPointerMove = useCallback((e: React.PointerEvent) => {
|
const onPointerMove = useCallback((e: React.PointerEvent) => {
|
||||||
@@ -44,24 +46,45 @@ export const useCardPointer = (
|
|||||||
updateState(e);
|
updateState(e);
|
||||||
|
|
||||||
if (state === 'drag-start') {
|
if (state === 'drag-start') {
|
||||||
|
let effectiveSelection = selection;
|
||||||
if (!isSelected) {
|
if (!isSelected) {
|
||||||
onSelect([card.id], hasModifier);
|
onSelect([card.id], hasModifier);
|
||||||
|
if (hasModifier) {
|
||||||
|
effectiveSelection = [...selection, card.id];
|
||||||
|
} else {
|
||||||
|
effectiveSelection = [card.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasModifier) {
|
|
||||||
card.bringToFront();
|
card.bringToFront();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setState('drag');
|
setState('drag');
|
||||||
}
|
|
||||||
|
|
||||||
if (state === 'drag' && initialPosition.current) {
|
if (initialPosition.current) {
|
||||||
const delta = {
|
const rect = card.ref.getBoundingClientRect();
|
||||||
x: e.clientX - initialPosition.current.x,
|
const centerX = rect.left + rect.width / 2;
|
||||||
y: e.clientY - initialPosition.current.y
|
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' && lastPosition.current) {
|
||||||
|
const delta = {
|
||||||
|
x: e.clientX - lastPosition.current.x,
|
||||||
|
y: e.clientY - lastPosition.current.y
|
||||||
|
};
|
||||||
|
|
||||||
|
if (delta.x !== 0 || delta.y !== 0) {
|
||||||
handleDragMove(card.store, selection, delta);
|
handleDragMove(card.store, selection, delta);
|
||||||
|
lastPosition.current = { x: e.clientX, y: e.clientY };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [card, state, updateState, isSelected, selection, onSelect]);
|
}, [card, state, updateState, isSelected, selection, onSelect]);
|
||||||
|
|
||||||
@@ -70,7 +93,7 @@ export const useCardPointer = (
|
|||||||
updateState(e);
|
updateState(e);
|
||||||
|
|
||||||
if (state === 'drag' || state === 'drag-start') {
|
if (state === 'drag' || state === 'drag-start') {
|
||||||
handleDragEnd();
|
handleDragEnd(card.store, selection);
|
||||||
} else if (state === 'click') {
|
} else if (state === 'click') {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
const isUnobstructed = card.isUnobstructed();
|
const isUnobstructed = card.isUnobstructed();
|
||||||
@@ -91,8 +114,9 @@ export const useCardPointer = (
|
|||||||
|
|
||||||
setState('idle');
|
setState('idle');
|
||||||
initialPosition.current = null;
|
initialPosition.current = null;
|
||||||
|
lastPosition.current = null;
|
||||||
(e.target as Element).releasePointerCapture(e.pointerId);
|
(e.target as Element).releasePointerCapture(e.pointerId);
|
||||||
}, [card, state, isSelected, onSelect, onDeselect, onDocumentActivate, updateState]);
|
}, [card, state, isSelected, onSelect, onDeselect, onDocumentActivate, updateState, selection]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onPointerDown,
|
onPointerDown,
|
||||||
|
|||||||
Reference in New Issue
Block a user