refactor(canvas): remove unused animation and optimize edge insertion timing
- Removed the unused CSS animation for edge insertion to streamline the codebase. - Updated the edge insertion reflow duration from 1297ms to 997ms for improved performance. - Refactored transition timing function for edge insertion to enhance animation smoothness. - Cleaned up related test cases to reflect the changes in edge insertion logic.
This commit is contained in:
@@ -145,17 +145,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes ls-edge-insert-enter {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
scale: 0.82;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
scale: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ls-connection-line-marching {
|
.ls-connection-line-marching {
|
||||||
animation: ls-connection-dash-offset 0.4s linear infinite;
|
animation: ls-connection-dash-offset 0.4s linear infinite;
|
||||||
}
|
}
|
||||||
@@ -262,17 +251,52 @@
|
|||||||
.react-flow.canvas-edge-insert-reflowing .react-flow__node {
|
.react-flow.canvas-edge-insert-reflowing .react-flow__node {
|
||||||
transition-property: transform;
|
transition-property: transform;
|
||||||
transition-duration: var(--ls-edge-insert-reflow-duration, 1297ms);
|
transition-duration: var(--ls-edge-insert-reflow-duration, 1297ms);
|
||||||
transition-timing-function: cubic-bezier(0.68, -0.6, 0.32, 1.6);
|
transition-timing-function: linear(
|
||||||
|
0 0%,
|
||||||
|
0.2718 2.5%,
|
||||||
|
0.6464 5%,
|
||||||
|
1 7.5%,
|
||||||
|
1.25 10%,
|
||||||
|
1.3641 12.5%,
|
||||||
|
1.3536 15%,
|
||||||
|
1.2575 17.5%,
|
||||||
|
1.125 20%,
|
||||||
|
1 22.5%,
|
||||||
|
0.9116 25%,
|
||||||
|
0.8713 27.5%,
|
||||||
|
0.875 30%,
|
||||||
|
0.909 32.5%,
|
||||||
|
0.9558 35%,
|
||||||
|
1 37.5%,
|
||||||
|
1.0313 40%,
|
||||||
|
1.0455 42.5%,
|
||||||
|
1.0442 45%,
|
||||||
|
1.0322 47.5%,
|
||||||
|
1.0156 50%,
|
||||||
|
1 52.5%,
|
||||||
|
0.989 55%,
|
||||||
|
0.9839 57.5%,
|
||||||
|
0.9844 60%,
|
||||||
|
0.9886 62.5%,
|
||||||
|
0.9945 65%,
|
||||||
|
1 67.5%,
|
||||||
|
1.0039 70%,
|
||||||
|
1.0057 72.5%,
|
||||||
|
1.0055 75%,
|
||||||
|
1.004 77.5%,
|
||||||
|
1.002 80%,
|
||||||
|
1 82.5%,
|
||||||
|
0.9986 85%,
|
||||||
|
0.998 87.5%,
|
||||||
|
0.998 90%,
|
||||||
|
0.9986 92.5%,
|
||||||
|
0.9993 95%,
|
||||||
|
1 97.5%,
|
||||||
|
1 100%
|
||||||
|
);
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__node.canvas-edge-insert-enter,
|
|
||||||
.react-flow__node .canvas-edge-insert-enter {
|
|
||||||
animation: ls-edge-insert-enter 300ms cubic-bezier(0.68, -0.6, 0.32, 1.6) both;
|
|
||||||
transform-origin: center center;
|
|
||||||
will-change: scale, opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.react-flow.canvas-scissors-mode .react-flow__edge:not(.temp) .react-flow__edge-path {
|
.react-flow.canvas-scissors-mode .react-flow__edge:not(.temp) .react-flow__edge-path {
|
||||||
transition: none;
|
transition: none;
|
||||||
@@ -281,13 +305,5 @@
|
|||||||
.react-flow.canvas-edge-insert-reflowing .react-flow__node {
|
.react-flow.canvas-edge-insert-reflowing .react-flow__node {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__node.canvas-edge-insert-enter,
|
|
||||||
.react-flow__node .canvas-edge-insert-enter {
|
|
||||||
animation: none;
|
|
||||||
opacity: 1;
|
|
||||||
scale: 1;
|
|
||||||
will-change: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,17 +90,9 @@ const latestHookValueRef: {
|
|||||||
|
|
||||||
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
function HookHarness({
|
function HookHarness({ canvasId }: { canvasId: Id<"canvases"> }) {
|
||||||
canvasId,
|
const [, setNodes] = useState<RFNode[]>([]);
|
||||||
initialNodes = [],
|
const [edges, setEdges] = useState<RFEdge[]>([]);
|
||||||
initialEdges = [],
|
|
||||||
}: {
|
|
||||||
canvasId: Id<"canvases">;
|
|
||||||
initialNodes?: RFNode[];
|
|
||||||
initialEdges?: RFEdge[];
|
|
||||||
}) {
|
|
||||||
const [nodes, setNodes] = useState<RFNode[]>(initialNodes);
|
|
||||||
const [edges, setEdges] = useState<RFEdge[]>(initialEdges);
|
|
||||||
const edgesRef = useRef<RFEdge[]>(edges);
|
const edgesRef = useRef<RFEdge[]>(edges);
|
||||||
const deletingNodeIds = useRef(new Set<string>());
|
const deletingNodeIds = useRef(new Set<string>());
|
||||||
const [, setAssetBrowserTargetNodeId] = useState<string | null>(null);
|
const [, setAssetBrowserTargetNodeId] = useState<string | null>(null);
|
||||||
@@ -128,15 +120,10 @@ function HookHarness({
|
|||||||
latestEdgesRef.current = edges;
|
latestEdgesRef.current = edges;
|
||||||
}, [edges]);
|
}, [edges]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
latestNodesRef.current = nodes;
|
|
||||||
}, [nodes]);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestEdgesRef: { current: RFEdge[] } = { current: [] };
|
const latestEdgesRef: { current: RFEdge[] } = { current: [] };
|
||||||
const latestNodesRef: { current: RFNode[] } = { current: [] };
|
|
||||||
|
|
||||||
function setNavigatorOnline(online: boolean) {
|
function setNavigatorOnline(online: boolean) {
|
||||||
Object.defineProperty(window.navigator, "onLine", {
|
Object.defineProperty(window.navigator, "onLine", {
|
||||||
@@ -152,7 +139,6 @@ describe("useCanvasSyncEngine hook wiring", () => {
|
|||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
latestHookValueRef.current = null;
|
latestHookValueRef.current = null;
|
||||||
latestEdgesRef.current = [];
|
latestEdgesRef.current = [];
|
||||||
latestNodesRef.current = [];
|
|
||||||
setNavigatorOnline(true);
|
setNavigatorOnline(true);
|
||||||
mocks.mutationMocks.clear();
|
mocks.mutationMocks.clear();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -295,97 +281,4 @@ describe("useCanvasSyncEngine hook wiring", () => {
|
|||||||
latestEdgesRef.current.some((edge) => edge.id === "optimistic_edge_req-2"),
|
latestEdgesRef.current.some((edge) => edge.id === "optimistic_edge_req-2"),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds optimistic split node and edges immediately in online mode before mutation resolves", async () => {
|
|
||||||
let createSplitResolver!: (value: string) => void;
|
|
||||||
const createSplitPromise = new Promise<string>((resolve) => {
|
|
||||||
createSplitResolver = resolve;
|
|
||||||
});
|
|
||||||
const createSplitMutation = vi.fn(() => createSplitPromise);
|
|
||||||
Object.assign(createSplitMutation, {
|
|
||||||
withOptimisticUpdate: () => createSplitMutation,
|
|
||||||
});
|
|
||||||
mocks.mutationMocks.set("nodes.createWithEdgeSplit", createSplitMutation);
|
|
||||||
|
|
||||||
container = document.createElement("div");
|
|
||||||
document.body.appendChild(container);
|
|
||||||
root = createRoot(container);
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
root?.render(
|
|
||||||
<HookHarness
|
|
||||||
canvasId={asCanvasId("canvas-1")}
|
|
||||||
initialNodes={[
|
|
||||||
{
|
|
||||||
id: "node-source",
|
|
||||||
type: "note",
|
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
data: {},
|
|
||||||
style: { width: 120, height: 80 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "node-target",
|
|
||||||
type: "note",
|
|
||||||
position: { x: 360, y: 0 },
|
|
||||||
data: {},
|
|
||||||
style: { width: 120, height: 80 },
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
initialEdges={[
|
|
||||||
{
|
|
||||||
id: "edge-base",
|
|
||||||
source: "node-source",
|
|
||||||
target: "node-target",
|
|
||||||
sourceHandle: "source-handle",
|
|
||||||
targetHandle: "target-handle",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let pendingCreateSplit: Promise<Id<"nodes">> | undefined;
|
|
||||||
await act(async () => {
|
|
||||||
pendingCreateSplit = latestHookValueRef.current?.actions.createNodeWithEdgeSplit({
|
|
||||||
canvasId: asCanvasId("canvas-1"),
|
|
||||||
type: "note",
|
|
||||||
positionX: 180,
|
|
||||||
positionY: 0,
|
|
||||||
width: 140,
|
|
||||||
height: 100,
|
|
||||||
data: {},
|
|
||||||
splitEdgeId: "edge-base" as Id<"edges">,
|
|
||||||
splitSourceHandle: "source-handle",
|
|
||||||
splitTargetHandle: "target-handle",
|
|
||||||
newNodeSourceHandle: "new-source-handle",
|
|
||||||
newNodeTargetHandle: "new-target-handle",
|
|
||||||
clientRequestId: "split-1",
|
|
||||||
});
|
|
||||||
await Promise.resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
latestNodesRef.current.find((node) => node.id === "optimistic_split-1"),
|
|
||||||
).toMatchObject({
|
|
||||||
className: "canvas-edge-insert-enter",
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
latestEdgesRef.current.some((edge) => edge.id === "edge-base"),
|
|
||||||
).toBe(false);
|
|
||||||
expect(
|
|
||||||
latestEdgesRef.current.some(
|
|
||||||
(edge) => edge.id === "optimistic_edge_split-1_split_a",
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
latestEdgesRef.current.some(
|
|
||||||
(edge) => edge.id === "optimistic_edge_split-1_split_b",
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
|
|
||||||
createSplitResolver("node-real-split-1");
|
|
||||||
await act(async () => {
|
|
||||||
await pendingCreateSplit;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ interface CanvasInnerProps {
|
|||||||
canvasId: Id<"canvases">;
|
canvasId: Id<"canvases">;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EDGE_INSERT_REFLOW_SETTLE_MS = 1297;
|
const EDGE_INSERT_REFLOW_SETTLE_MS = 997;
|
||||||
|
|
||||||
function CanvasInner({ canvasId }: CanvasInnerProps) {
|
function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||||
const t = useTranslations('toasts');
|
const t = useTranslations('toasts');
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export type EdgeInsertMenuState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const EDGE_INSERT_GAP_PX = 10;
|
const EDGE_INSERT_GAP_PX = 10;
|
||||||
const DEFAULT_REFLOW_SETTLE_MS = 1297;
|
const DEFAULT_REFLOW_SETTLE_MS = 997;
|
||||||
|
|
||||||
function waitForReflowSettle(ms: number): Promise<void> {
|
function waitForReflowSettle(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
|||||||
@@ -746,6 +746,8 @@ export function useCanvasSyncEngine({
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createNodeWithEdgeSplitMut = useMutation(api.nodes.createWithEdgeSplit);
|
||||||
|
|
||||||
const createEdge = useMutation(api.edges.create).withOptimisticUpdate(
|
const createEdge = useMutation(api.edges.create).withOptimisticUpdate(
|
||||||
(localStore, args) => {
|
(localStore, args) => {
|
||||||
const edgeList = localStore.getQuery(api.edges.list, {
|
const edgeList = localStore.getQuery(api.edges.list, {
|
||||||
@@ -847,10 +849,7 @@ export function useCanvasSyncEngine({
|
|||||||
|
|
||||||
const addOptimisticNodeLocally = useCallback(
|
const addOptimisticNodeLocally = useCallback(
|
||||||
(
|
(
|
||||||
args: Parameters<typeof createNode>[0] & {
|
args: Parameters<typeof createNode>[0] & { clientRequestId: string },
|
||||||
clientRequestId: string;
|
|
||||||
className?: string;
|
|
||||||
},
|
|
||||||
): Id<"nodes"> => {
|
): Id<"nodes"> => {
|
||||||
const optimisticNodeId = `${OPTIMISTIC_NODE_PREFIX}${args.clientRequestId}`;
|
const optimisticNodeId = `${OPTIMISTIC_NODE_PREFIX}${args.clientRequestId}`;
|
||||||
setNodes((current) => {
|
setNodes((current) => {
|
||||||
@@ -867,7 +866,6 @@ export function useCanvasSyncEngine({
|
|||||||
style: { width: args.width, height: args.height },
|
style: { width: args.width, height: args.height },
|
||||||
parentId: args.parentId as string | undefined,
|
parentId: args.parentId as string | undefined,
|
||||||
zIndex: args.zIndex,
|
zIndex: args.zIndex,
|
||||||
className: args.className,
|
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -1392,24 +1390,15 @@ export function useCanvasSyncEngine({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const runCreateNodeWithEdgeSplitOnlineOnly = useCallback(
|
const runCreateNodeWithEdgeSplitOnlineOnly = useCallback(
|
||||||
async (args: Parameters<typeof createNodeWithEdgeSplitRaw>[0]) => {
|
async (args: Parameters<typeof createNodeWithEdgeSplitMut>[0]) => {
|
||||||
const clientRequestId = args.clientRequestId ?? crypto.randomUUID();
|
const clientRequestId = args.clientRequestId ?? crypto.randomUUID();
|
||||||
const payload = { ...args, clientRequestId };
|
const payload = { ...args, clientRequestId };
|
||||||
const splitEdgeId = payload.splitEdgeId as string;
|
|
||||||
|
|
||||||
controller.pendingConnectionCreatesRef.current.add(clientRequestId);
|
if (isSyncOnline) {
|
||||||
|
return await createNodeWithEdgeSplitMut(payload);
|
||||||
|
}
|
||||||
|
|
||||||
const originalSplitEdge = edgesRef.current.find(
|
const optimisticNodeId = addOptimisticNodeLocally(payload);
|
||||||
(edge) =>
|
|
||||||
edge.id === splitEdgeId &&
|
|
||||||
edge.className !== "temp" &&
|
|
||||||
!isOptimisticEdgeId(edge.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
const optimisticNodeId = addOptimisticNodeLocally({
|
|
||||||
...payload,
|
|
||||||
className: "canvas-edge-insert-enter",
|
|
||||||
});
|
|
||||||
const splitApplied = applyEdgeSplitLocally({
|
const splitApplied = applyEdgeSplitLocally({
|
||||||
clientRequestId,
|
clientRequestId,
|
||||||
splitEdgeId: payload.splitEdgeId,
|
splitEdgeId: payload.splitEdgeId,
|
||||||
@@ -1422,34 +1411,6 @@ export function useCanvasSyncEngine({
|
|||||||
positionY: payload.positionY,
|
positionY: payload.positionY,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isSyncOnline) {
|
|
||||||
try {
|
|
||||||
const realId = await trackPendingNodeCreate(
|
|
||||||
clientRequestId,
|
|
||||||
createNodeWithEdgeSplitRaw({ ...payload }),
|
|
||||||
);
|
|
||||||
await remapOptimisticNodeLocally(clientRequestId, realId);
|
|
||||||
return realId;
|
|
||||||
} catch (error) {
|
|
||||||
removeOptimisticCreateLocally({
|
|
||||||
clientRequestId,
|
|
||||||
removeNode: true,
|
|
||||||
removeEdge: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (splitApplied && originalSplitEdge) {
|
|
||||||
setEdges((current) => {
|
|
||||||
if (current.some((edge) => edge.id === originalSplitEdge.id)) {
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
return [...current, originalSplitEdge];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (splitApplied) {
|
if (splitApplied) {
|
||||||
await enqueueSyncMutation("createNodeWithEdgeSplit", payload);
|
await enqueueSyncMutation("createNodeWithEdgeSplit", payload);
|
||||||
} else {
|
} else {
|
||||||
@@ -1469,19 +1430,7 @@ export function useCanvasSyncEngine({
|
|||||||
|
|
||||||
return optimisticNodeId;
|
return optimisticNodeId;
|
||||||
},
|
},
|
||||||
[
|
[addOptimisticNodeLocally, applyEdgeSplitLocally, createNodeWithEdgeSplitMut, enqueueSyncMutation, isSyncOnline],
|
||||||
addOptimisticNodeLocally,
|
|
||||||
applyEdgeSplitLocally,
|
|
||||||
controller.pendingConnectionCreatesRef,
|
|
||||||
createNodeWithEdgeSplitRaw,
|
|
||||||
edgesRef,
|
|
||||||
enqueueSyncMutation,
|
|
||||||
isSyncOnline,
|
|
||||||
remapOptimisticNodeLocally,
|
|
||||||
removeOptimisticCreateLocally,
|
|
||||||
setEdges,
|
|
||||||
trackPendingNodeCreate,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const runBatchRemoveNodesMutation = useCallback<RunBatchRemoveNodesMutation>(
|
const runBatchRemoveNodesMutation = useCallback<RunBatchRemoveNodesMutation>(
|
||||||
|
|||||||
Reference in New Issue
Block a user