데스크탑 앱 전략편집기 수정
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
type Node,
|
||||
type OnNodesChange,
|
||||
type OnSelectionChangeFunc,
|
||||
type ReactFlowInstance,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import type { Theme } from '../../types/index';
|
||||
@@ -36,6 +37,10 @@ import {
|
||||
subscribePaletteDrag,
|
||||
type PaletteDragEvent,
|
||||
} from '../../utils/paletteDragSession';
|
||||
import {
|
||||
isOverStrategyBuilder,
|
||||
projectScreenToFlowPosition,
|
||||
} from '../../utils/flowScreenProjection';
|
||||
import { setStochPairSecondary } from '../../utils/stochOverboughtPair';
|
||||
import {
|
||||
setPriceExtremePairFilterLevel,
|
||||
@@ -296,6 +301,8 @@ function StrategyEditorCanvasInner({
|
||||
onExtraRootsChange,
|
||||
}: Props) {
|
||||
const { fitView, screenToFlowPosition } = useReactFlow();
|
||||
const canvasWrapRef = useRef<HTMLDivElement>(null);
|
||||
const reactFlowRef = useRef<ReactFlowInstance | null>(null);
|
||||
const positionsRef = useRef(new Map<string, { x: number; y: number }>());
|
||||
const edgeHandlesRef = useRef(new Map<string, EdgeHandleBinding>());
|
||||
const structureKeyRef = useRef<string | null>(null);
|
||||
@@ -610,16 +617,16 @@ function StrategyEditorCanvasInner({
|
||||
anchorId: string,
|
||||
data: { type: string; value: string; label: string; composite?: boolean },
|
||||
flowPos: { x: number; y: number },
|
||||
) => {
|
||||
): boolean => {
|
||||
if (data.type === 'start') {
|
||||
addStartAt(flowPos);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
const newNode = resolvePaletteDropNode(data);
|
||||
if (!newNode) return;
|
||||
if (!newNode) return false;
|
||||
const anchor = nodesRef.current.find(n => n.id === anchorId);
|
||||
if (!anchor) return;
|
||||
if (!anchor) return false;
|
||||
|
||||
const anchorDim = getNodeDimensions(anchorId);
|
||||
const { sourceHandle, targetHandle, sourceSide } = connectionSidesFromDrop(anchor.position, anchorDim, flowPos);
|
||||
@@ -641,32 +648,38 @@ function StrategyEditorCanvasInner({
|
||||
}
|
||||
saveAttachHandles(anchorId);
|
||||
scheduleLayoutEmit();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
const anchorNode = findNodeInTree(root, anchorId)
|
||||
?? Object.values(extraRoots).map(br => findNodeInTree(br, anchorId)).find(Boolean);
|
||||
if (anchorNode?.type === 'CONDITION') {
|
||||
onChange(mergeAtRoot(root, newNode, data.type === 'operator'));
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!canAcceptPaletteAsParent(anchorId, root)) return;
|
||||
if (!canAcceptPaletteAsParent(anchorId, root)) return false;
|
||||
|
||||
if (root && findNodeInTree(root, anchorId)) {
|
||||
onChange(addChild(root, anchorId, newNode));
|
||||
} else {
|
||||
const parentBranch = Object.entries(extraRoots).find(([, br]) => br && findNodeInTree(br, anchorId));
|
||||
if (parentBranch) {
|
||||
const [sid, br] = parentBranch;
|
||||
onExtraRootsChange?.({
|
||||
...extraRoots,
|
||||
[sid]: addChild(br!, anchorId, newNode),
|
||||
});
|
||||
}
|
||||
saveAttachHandles(anchorId);
|
||||
scheduleLayoutEmit();
|
||||
return true;
|
||||
}
|
||||
saveAttachHandles(anchorId);
|
||||
scheduleLayoutEmit();
|
||||
|
||||
const parentBranch = Object.entries(extraRoots).find(([, br]) => br && findNodeInTree(br, anchorId));
|
||||
if (parentBranch) {
|
||||
const [sid, br] = parentBranch;
|
||||
onExtraRootsChange?.({
|
||||
...extraRoots,
|
||||
[sid]: addChild(br!, anchorId, newNode),
|
||||
});
|
||||
saveAttachHandles(anchorId);
|
||||
scheduleLayoutEmit();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [root, extraRoots, onChange, onExtraRootsChange, addStartAt, scheduleLayoutEmit, resolvePaletteDropNode]);
|
||||
|
||||
const handleDragOverTarget = useCallback((anchorId: string, flowPos: { x: number; y: number }) => {
|
||||
@@ -688,7 +701,8 @@ function StrategyEditorCanvasInner({
|
||||
) => {
|
||||
clearDropPreview();
|
||||
if (isPaletteConnectTarget(anchorId, root, orphans)) {
|
||||
applyDropWithAttach(anchorId, data, flowPos);
|
||||
const attached = applyDropWithAttach(anchorId, data, flowPos);
|
||||
if (!attached) addOrphanAt(data, flowPos);
|
||||
} else {
|
||||
addOrphanAt(data, flowPos);
|
||||
}
|
||||
@@ -1207,23 +1221,39 @@ function StrategyEditorCanvasInner({
|
||||
|
||||
const isPaletteDrag = (e: React.DragEvent) => isPaletteHtmlDrag(e);
|
||||
|
||||
const clientToFlow = useCallback((clientX: number, clientY: number) => {
|
||||
const rf = reactFlowRef.current;
|
||||
if (rf) {
|
||||
return projectScreenToFlowPosition(clientX, clientY, rf, canvasWrapRef.current);
|
||||
}
|
||||
return screenToFlowPosition({ x: clientX, y: clientY });
|
||||
}, [screenToFlowPosition]);
|
||||
|
||||
const applyPaletteDropAt = useCallback((
|
||||
data: { type: string; value: string; label: string; composite?: boolean },
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
) => {
|
||||
const flowPos = screenToFlowPosition({ x: clientX, y: clientY });
|
||||
if (!isOverStrategyBuilder(clientX, clientY)) return;
|
||||
|
||||
const flowPos = clientToFlow(clientX, clientY);
|
||||
if (data.type === 'start') {
|
||||
addStartAt(flowPos);
|
||||
return;
|
||||
}
|
||||
const drop = resolvePaletteDrop(flowPos);
|
||||
if (drop.mode === 'connect') {
|
||||
applyDropWithAttach(drop.anchorId, data, flowPos);
|
||||
} else {
|
||||
addOrphanAt(data, flowPos);
|
||||
|
||||
const hit = findNodeAtFlowPosition(flowPos, nodesRef.current);
|
||||
if (hit && isPaletteConnectTarget(hit.id, root, orphans)) {
|
||||
handleDropTarget(hit.id, data, flowPos);
|
||||
return;
|
||||
}
|
||||
}, [screenToFlowPosition, applyDropWithAttach, addOrphanAt, addStartAt, resolvePaletteDrop]);
|
||||
|
||||
addOrphanAt(data, flowPos);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
reactFlowRef.current?.fitView({ padding: 0.35, duration: 180 });
|
||||
});
|
||||
}, [clientToFlow, addStartAt, handleDropTarget, addOrphanAt, root, orphans]);
|
||||
|
||||
const onPaneDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -1233,48 +1263,57 @@ function StrategyEditorCanvasInner({
|
||||
applyPaletteDropAt(data, e.clientX, e.clientY);
|
||||
}, [clearDropPreview, applyPaletteDropAt]);
|
||||
|
||||
const applyDropRef = useRef(applyPaletteDropAt);
|
||||
applyDropRef.current = applyPaletteDropAt;
|
||||
const clearPreviewRef = useRef(clearDropPreview);
|
||||
clearPreviewRef.current = clearDropPreview;
|
||||
const resolveDropRef = useRef(resolvePaletteDrop);
|
||||
resolveDropRef.current = resolvePaletteDrop;
|
||||
const updatePreviewRef = useRef(updateDropPreview);
|
||||
updatePreviewRef.current = updateDropPreview;
|
||||
const clientToFlowRef = useRef(clientToFlow);
|
||||
clientToFlowRef.current = clientToFlow;
|
||||
|
||||
useEffect(() => {
|
||||
if (!needsPointerPaletteDrag()) return;
|
||||
return subscribePaletteDrag((ev: PaletteDragEvent) => {
|
||||
if (ev.phase === 'move') {
|
||||
const flowPos = screenToFlowPosition({ x: ev.x, y: ev.y });
|
||||
const drop = resolvePaletteDrop(flowPos);
|
||||
if (!isOverStrategyBuilder(ev.x, ev.y)) {
|
||||
clearPreviewRef.current();
|
||||
return;
|
||||
}
|
||||
const flowPos = clientToFlowRef.current(ev.x, ev.y);
|
||||
const drop = resolveDropRef.current(flowPos);
|
||||
if (drop.mode === 'connect') {
|
||||
updateDropPreview(drop.anchorId, flowPos);
|
||||
updatePreviewRef.current(drop.anchorId, flowPos);
|
||||
} else {
|
||||
clearDropPreview();
|
||||
clearPreviewRef.current();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ev.phase === 'end') {
|
||||
clearDropPreview();
|
||||
applyPaletteDropAt(ev.payload, ev.x, ev.y);
|
||||
clearPreviewRef.current();
|
||||
applyDropRef.current(ev.payload, ev.x, ev.y);
|
||||
return;
|
||||
}
|
||||
if (ev.phase === 'cancel') {
|
||||
clearDropPreview();
|
||||
clearPreviewRef.current();
|
||||
}
|
||||
});
|
||||
}, [
|
||||
screenToFlowPosition,
|
||||
resolvePaletteDrop,
|
||||
updateDropPreview,
|
||||
clearDropPreview,
|
||||
applyPaletteDropAt,
|
||||
]);
|
||||
}, []);
|
||||
|
||||
const onPaneDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
if (!isPaletteDrag(e)) return;
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
const flowPos = screenToFlowPosition({ x: e.clientX, y: e.clientY });
|
||||
const flowPos = clientToFlow(e.clientX, e.clientY);
|
||||
const drop = resolvePaletteDrop(flowPos);
|
||||
if (drop.mode === 'connect') {
|
||||
updateDropPreview(drop.anchorId, flowPos);
|
||||
} else {
|
||||
clearDropPreview();
|
||||
}
|
||||
}, [screenToFlowPosition, resolvePaletteDrop, updateDropPreview, clearDropPreview]);
|
||||
}, [clientToFlow, resolvePaletteDrop, updateDropPreview, clearDropPreview]);
|
||||
|
||||
const onPaneDragLeave = useCallback((e: React.DragEvent) => {
|
||||
const rel = e.relatedTarget as Element | null;
|
||||
@@ -1304,11 +1343,13 @@ function StrategyEditorCanvasInner({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={canvasWrapRef}
|
||||
className={`se-canvas-wrap se-canvas-wrap--${signalTab} se-canvas-wrap--${interactionMode}`}
|
||||
onKeyDown={onKeyDown}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ReactFlow
|
||||
onInit={inst => { reactFlowRef.current = inst; }}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
|
||||
Reference in New Issue
Block a user