Skip to content

Commit

Permalink
Fix node and wire flickering
Browse files Browse the repository at this point in the history
  • Loading branch information
abrenneke committed May 10, 2024
1 parent 6658904 commit 54836c3
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 47 deletions.
8 changes: 7 additions & 1 deletion packages/app/src/components/NodeBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
globalRivetNodeRegistry,
type NodeBody as RenderedNodeBody,
getError,
type NodeId,
} from '@ironclad/rivet-core';
import { useMarkdown } from '../hooks/useMarkdown';
import { match } from 'ts-pattern';
Expand Down Expand Up @@ -40,10 +41,13 @@ const UnknownNodeBodyWrapper = styled.div<{
font-family: ${(props) => (props.fontFamily === 'monospace' ? "'Roboto Mono', monospace" : "'Roboto', sans-serif")};
`;

// Fixes flickering due to async rendering of node body by caching the last rendered body
const previousRenderedBodyMap = new Map<NodeId, RenderedNodeBody>();

const UnknownNodeBody: FC<{ heightCache: HeightCache; node: ChartNode }> = ({ heightCache, node }) => {
const getUIContext = useGetRivetUIContext();

const [body, setBody] = useState<RenderedNodeBody | undefined>();
const [body, setBody] = useState<RenderedNodeBody | undefined>(previousRenderedBodyMap.get(node.id));
const { ref, height } = useNodeBodyHeight(heightCache, node.id, !!body);

useAsyncEffect(async () => {
Expand All @@ -53,6 +57,8 @@ const UnknownNodeBody: FC<{ heightCache: HeightCache; node: ChartNode }> = ({ he
const renderedBody = await impl.getBody(await getUIContext({ node }));

setBody(renderedBody);

previousRenderedBodyMap.set(node.id, renderedBody);
} catch (err) {
toast.error(`Failed to load body for node ${node.id}: ${getError(err).message}`);
}
Expand Down
13 changes: 8 additions & 5 deletions packages/app/src/components/NodeCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ export const NodeCanvas: FC<NodeCanvasProps> = ({
nodePortPositions,
canvasRef,
recalculate: recalculatePortPositions,
} = useNodePortPositions({ enabled: shouldRenderWires });
} = useNodePortPositions({ enabled: shouldRenderWires, isDraggingNode: draggingNodes.length > 0 });

useEffect(() => {
recalculatePortPositions();
Expand Down Expand Up @@ -594,6 +594,8 @@ export const NodeCanvas: FC<NodeCanvasProps> = ({
const isLargeGraph = nodes.length > 100;
const debounceTime = isLargeGraph ? 500 : 50;

const previousNodes = useRef<ChartNode[]>([]);

useLayoutEffect(() => {
const recalculateVisibleNodes = () => {
let numVisible = 0;
Expand All @@ -608,10 +610,6 @@ export const NodeCanvas: FC<NodeCanvasProps> = ({
node.visualData.y > viewportBounds.bottom + 500) &&
!isPinned;

// if (numVisible > 300) {
// shouldHide = true;
// }

shouldShowNodeBasedOnPosition.set(node, !shouldHide);

if (!shouldHide) {
Expand All @@ -630,6 +628,11 @@ export const NodeCanvas: FC<NodeCanvasProps> = ({
movingRerenderTimeout.current = window.setTimeout(() => {
recalculateVisibleNodes();
}, debounceTime);

if (previousNodes.current !== nodes) {
previousNodes.current = nodes;
recalculateVisibleNodes();
}
}, [
pinnedNodes,
nodes,
Expand Down
91 changes: 50 additions & 41 deletions packages/app/src/hooks/useNodePortPositions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { nodesByIdState } from '../state/graph';
* It's done this way with a nodePortPositions state using rounded numbers for performance reasons.
* In the ideal case, no position will have changed, so the state does not update.
*/
export function useNodePortPositions({ enabled }: { enabled: boolean }) {
export function useNodePortPositions({ enabled, isDraggingNode }: { enabled: boolean; isDraggingNode: boolean }) {
const [nodePortPositions, setNodePortPositions] = useState<PortPositions>({});
const nodesById = useRecoilValue(nodesByIdState);
const canvasRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -60,65 +60,74 @@ export function useNodePortPositions({ enabled }: { enabled: boolean }) {
y: Math.round((nodePos.y + positionFromNode.top + elem.offsetHeight / 2) * precision) / precision,
};

if (nodePortPositions[key]?.x !== pos.x || nodePortPositions[key]?.y !== pos.y) {
const prevPos = nodePortPositions[key];

if (prevPos?.x !== pos.x || prevPos?.y !== pos.y) {
changed = true;
newPositions[key] = pos;
}

seen.add(key);
}

const overlayPortElements = canvasRef.current?.querySelectorAll(
'.overlayNode .port-circle',
) as NodeListOf<HTMLDivElement>;
// Fixes a rendering issue where when you drag a node, for one frame the node.visualData.x and node.visualData.y have been updated
// to the new position, but the overlay is still active moving the node by the same amount, which causes the wires to flicker
// as for one frame they move double the distance.
if (isDraggingNode) {
const overlayPortElements = canvasRef.current?.querySelectorAll(
'.overlayNode .port-circle',
) as NodeListOf<HTMLDivElement>;

for (const elem of overlayPortElements) {
const nodeElem = elem.closest('.node') as HTMLElement;
for (const elem of overlayPortElements) {
const nodeElem = elem.closest('.node') as HTMLElement;

const portId = elem.dataset.portid! as PortId;
const nodeId = elem.dataset.nodeid! as NodeId;
const portType = elem.dataset.porttype! as 'input' | 'output';
const key = `${nodeId}-${portType}-${portId}`;
const portId = elem.dataset.portid! as PortId;
const nodeId = elem.dataset.nodeid! as NodeId;
const portType = elem.dataset.porttype! as 'input' | 'output';
const key = `${nodeId}-${portType}-${portId}`;

if (seen.has(key)) {
return;
}
if (seen.has(key)) {
return;
}

const node = nodesById[nodeId]!;
const node = nodesById[nodeId]!;

const nodePos = { x: node.visualData.x, y: node.visualData.y };
const nodePos = { x: node.visualData.x, y: node.visualData.y };

// For the overlay nodes, they have an additional transform on the parent element, so we need to account for that
const overlayPositionedElement = nodeElem.offsetParent as HTMLDivElement;
const translate3dRegexMatch = overlayPositionedElement?.style.transform?.match(
/translate3d\((?:([\d.-]+)(?:px?)), *(?:([\d.-]+)(?:px?)), *(?:([\d.-]+)(?:px?))?\)/,
);
const [, x, y] = translate3dRegexMatch ?? [];
// For the overlay nodes, they have an additional transform on the parent element, so we need to account for that
const overlayPositionedElement = nodeElem.offsetParent as HTMLDivElement;
const translate3dRegexMatch = overlayPositionedElement?.style.transform?.match(
/translate3d\((?:([\d.-]+)(?:px?)), *(?:([\d.-]+)(?:px?)), *(?:([\d.-]+)(?:px?))?\)/,
);
const [, x, y] = translate3dRegexMatch ?? [];

if (x && y) {
nodePos.x += parseFloat(x || '0');
nodePos.y += parseFloat(y || '0');
}
if (x && y) {
nodePos.x += parseFloat(x || '0');
nodePos.y += parseFloat(y || '0');
}

const positionFromNode = { left: elem.offsetLeft, top: elem.offsetTop };
let elemParent = elem.offsetParent as HTMLElement | undefined;
const positionFromNode = { left: elem.offsetLeft, top: elem.offsetTop };
let elemParent = elem.offsetParent as HTMLElement | undefined;

while (!elemParent?.classList.contains('node')) {
positionFromNode.left += elemParent?.offsetLeft ?? 0;
positionFromNode.top += elemParent?.offsetTop ?? 0;
elemParent = elemParent?.offsetParent as HTMLElement | undefined;
}
while (!elemParent?.classList.contains('node')) {
positionFromNode.left += elemParent?.offsetLeft ?? 0;
positionFromNode.top += elemParent?.offsetTop ?? 0;
elemParent = elemParent?.offsetParent as HTMLElement | undefined;
}

const precision = 10;
const precision = 10;

const pos = {
x: Math.round((nodePos.x + positionFromNode.left + elem.offsetWidth / 2) * precision) / precision,
y: Math.round((nodePos.y + positionFromNode.top + elem.offsetHeight / 2) * precision) / precision,
};
const pos = {
x: Math.round((nodePos.x + positionFromNode.left + elem.offsetWidth / 2) * precision) / precision,
y: Math.round((nodePos.y + positionFromNode.top + elem.offsetHeight / 2) * precision) / precision,
};

if (nodePortPositions[key]?.x !== pos.x || nodePortPositions[key]?.y !== pos.y) {
changed = true;
newPositions[key] = pos;
const prevPos = nodePortPositions[key];

if (prevPos?.x !== pos.x || prevPos?.y !== pos.y) {
changed = true;
newPositions[key] = pos;
}
}
}

Expand Down

0 comments on commit 54836c3

Please sign in to comment.