전략편집기 수정
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
getNodeDimensions,
|
||||
pickSideFromPoint,
|
||||
START_NODE_ID,
|
||||
type HandleSide,
|
||||
} from './strategyFlowLayout';
|
||||
|
||||
export type EdgeDragZone = 'source' | 'middle' | 'target';
|
||||
|
||||
/** 연결선 상 클릭/드래그 위치 → 좌(소스) / 중(경로) / 우(타겟) 구간 */
|
||||
export function getEdgeDragZone(
|
||||
sourceX: number,
|
||||
sourceY: number,
|
||||
targetX: number,
|
||||
targetY: number,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
): EdgeDragZone {
|
||||
const dx = targetX - sourceX;
|
||||
const dy = targetY - sourceY;
|
||||
const len2 = dx * dx + dy * dy;
|
||||
if (len2 < 4) return 'middle';
|
||||
const t = ((pointerX - sourceX) * dx + (pointerY - sourceY) * dy) / len2;
|
||||
const clamped = Math.max(0, Math.min(1, t));
|
||||
if (clamped < 1 / 3) return 'source';
|
||||
if (clamped > 2 / 3) return 'target';
|
||||
return 'middle';
|
||||
}
|
||||
|
||||
function sideToSourceHandle(side: HandleSide): string {
|
||||
return `s-${side}`;
|
||||
}
|
||||
|
||||
function sideToTargetHandle(side: HandleSide): string {
|
||||
return `t-${side}`;
|
||||
}
|
||||
|
||||
/** 드래그 위치에 맞는 소스(출발) 핸들 */
|
||||
export function pickSourceHandleAtPoint(
|
||||
nodeId: string,
|
||||
flowPos: { x: number; y: number },
|
||||
positions: Map<string, { x: number; y: number }>,
|
||||
): string {
|
||||
const pos = positions.get(nodeId) ?? { x: 0, y: 0 };
|
||||
const dim = getNodeDimensions(nodeId);
|
||||
return sideToSourceHandle(pickSideFromPoint(flowPos, pos, dim));
|
||||
}
|
||||
|
||||
/** 드래그 위치에 맞는 타겟(도착) 핸들 */
|
||||
export function pickTargetHandleAtPoint(
|
||||
nodeId: string,
|
||||
flowPos: { x: number; y: number },
|
||||
positions: Map<string, { x: number; y: number }>,
|
||||
): string {
|
||||
const pos = positions.get(nodeId) ?? { x: 0, y: 0 };
|
||||
const dim = getNodeDimensions(nodeId);
|
||||
return sideToTargetHandle(pickSideFromPoint(flowPos, pos, dim));
|
||||
}
|
||||
|
||||
export function buildPositionsFromNodes(
|
||||
nodes: { id: string; position: { x: number; y: number } }[],
|
||||
): Map<string, { x: number; y: number }> {
|
||||
const map = new Map<string, { x: number; y: number }>();
|
||||
for (const n of nodes) {
|
||||
map.set(n.id, { x: n.position.x, y: n.position.y });
|
||||
}
|
||||
if (!map.has(START_NODE_ID)) {
|
||||
map.set(START_NODE_ID, { x: 0, y: 220 });
|
||||
}
|
||||
return map;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { LogicNode } from './strategyTypes';
|
||||
import type { EdgeHandleBinding } from './strategyFlowLayout';
|
||||
|
||||
export type SignalFlowLayoutSnapshot = {
|
||||
positions: Record<string, { x: number; y: number }>;
|
||||
edgeHandles: Record<string, EdgeHandleBinding>;
|
||||
orphans?: LogicNode[];
|
||||
};
|
||||
|
||||
export type FlowLayoutChangePayload = Omit<SignalFlowLayoutSnapshot, 'orphans'> & {
|
||||
tab: 'buy' | 'sell';
|
||||
};
|
||||
|
||||
export type StrategyFlowLayoutStore = {
|
||||
buy: SignalFlowLayoutSnapshot;
|
||||
sell: SignalFlowLayoutSnapshot;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'gc_se_flow_layout_v1';
|
||||
|
||||
export function emptySignalFlowLayout(): SignalFlowLayoutSnapshot {
|
||||
return { positions: {}, edgeHandles: {}, orphans: [] };
|
||||
}
|
||||
|
||||
export function emptyStrategyFlowLayout(): StrategyFlowLayoutStore {
|
||||
return { buy: emptySignalFlowLayout(), sell: emptySignalFlowLayout() };
|
||||
}
|
||||
|
||||
export function loadStrategyFlowLayout(strategyKey: string): StrategyFlowLayoutStore | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const all = JSON.parse(raw) as Record<string, StrategyFlowLayoutStore>;
|
||||
return all[strategyKey] ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveStrategyFlowLayout(strategyKey: string, layout: StrategyFlowLayoutStore): void {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
const all: Record<string, StrategyFlowLayoutStore> = raw ? JSON.parse(raw) : {};
|
||||
all[strategyKey] = layout;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(all));
|
||||
} catch {
|
||||
/* ignore quota / private mode */
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteStrategyFlowLayout(strategyKey: string): void {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return;
|
||||
const all = JSON.parse(raw) as Record<string, StrategyFlowLayoutStore>;
|
||||
delete all[strategyKey];
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(all));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
export function migrateStrategyFlowLayout(fromKey: string, toKey: string): void {
|
||||
if (fromKey === toKey) return;
|
||||
const layout = loadStrategyFlowLayout(fromKey);
|
||||
if (!layout) return;
|
||||
saveStrategyFlowLayout(toKey, layout);
|
||||
deleteStrategyFlowLayout(fromKey);
|
||||
}
|
||||
@@ -301,7 +301,13 @@ export function buildConnectionFromPositions(
|
||||
export type EdgeHandleBinding = {
|
||||
sourceHandle?: string | null;
|
||||
targetHandle?: string | null;
|
||||
/** smooth-step 경로 굽힘 중심 (중간 영역 드래그) */
|
||||
centerX?: number | null;
|
||||
centerY?: number | null;
|
||||
manual?: boolean;
|
||||
manualSource?: boolean;
|
||||
manualTarget?: boolean;
|
||||
manualCenter?: boolean;
|
||||
};
|
||||
|
||||
export const FLOW_NODE_W = 200;
|
||||
@@ -340,21 +346,69 @@ export function pickHandleSides(
|
||||
: { sourceHandle: 's-top', targetHandle: 't-bottom' };
|
||||
}
|
||||
|
||||
/** 경로 굽힘 중심 기준 — 양쪽 노드에 가장 자연스러운 핸들 */
|
||||
export function pickHandlesFromCenter(
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
center: { x: number; y: number },
|
||||
positions: Map<string, { x: number; y: number }>,
|
||||
): { sourceHandle: string; targetHandle: string } {
|
||||
const sourcePos = positions.get(sourceId) ?? { x: 0, y: 0 };
|
||||
const targetPos = positions.get(targetId) ?? { x: 0, y: 0 };
|
||||
const sourceDim = getNodeDimensions(sourceId);
|
||||
const targetDim = getNodeDimensions(targetId);
|
||||
const sourceSide = pickSideFromPoint(center, sourcePos, sourceDim);
|
||||
const targetSide = pickSideFromPoint(center, targetPos, targetDim);
|
||||
return {
|
||||
sourceHandle: `s-${sourceSide}`,
|
||||
targetHandle: `t-${targetSide}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyEdgeHandles(
|
||||
edges: Edge[],
|
||||
positions: Map<string, { x: number; y: number }>,
|
||||
saved: Map<string, EdgeHandleBinding>,
|
||||
): Edge[] {
|
||||
return edges.map(edge => {
|
||||
const binding = saved.get(edge.id);
|
||||
if (binding?.manual && binding.sourceHandle && binding.targetHandle) {
|
||||
return { ...edge, sourceHandle: binding.sourceHandle, targetHandle: binding.targetHandle };
|
||||
}
|
||||
const binding = saved.get(edge.id) ?? {};
|
||||
const picked = pickHandleSides(edge.source, edge.target, positions);
|
||||
const sourceHandle = binding?.sourceHandle ?? picked.sourceHandle;
|
||||
const targetHandle = binding?.targetHandle ?? picked.targetHandle;
|
||||
saved.set(edge.id, { sourceHandle, targetHandle, manual: binding?.manual });
|
||||
return { ...edge, sourceHandle, targetHandle };
|
||||
let sourceHandle = picked.sourceHandle;
|
||||
let targetHandle = picked.targetHandle;
|
||||
|
||||
if (
|
||||
binding.manualCenter
|
||||
&& binding.centerX != null
|
||||
&& binding.centerY != null
|
||||
) {
|
||||
const natural = pickHandlesFromCenter(
|
||||
edge.source,
|
||||
edge.target,
|
||||
{ x: binding.centerX, y: binding.centerY },
|
||||
positions,
|
||||
);
|
||||
if (!binding.manualSource) sourceHandle = natural.sourceHandle;
|
||||
else sourceHandle = binding.sourceHandle ?? picked.sourceHandle;
|
||||
if (!binding.manualTarget) targetHandle = natural.targetHandle;
|
||||
else targetHandle = binding.targetHandle ?? picked.targetHandle;
|
||||
} else {
|
||||
if (binding.manualSource) sourceHandle = binding.sourceHandle ?? picked.sourceHandle;
|
||||
if (binding.manualTarget) targetHandle = binding.targetHandle ?? picked.targetHandle;
|
||||
}
|
||||
|
||||
const nextBinding: EdgeHandleBinding = {
|
||||
...binding,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
};
|
||||
saved.set(edge.id, nextBinding);
|
||||
|
||||
return {
|
||||
...edge,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
data: { ...(edge.data as object), binding: nextBinding },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user