Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .storybook/preview.ts

This file was deleted.

16 changes: 16 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Preview } from "@storybook/react";
import React, { StrictMode } from "react";

import './styles/global.css';

const preview: Preview = {
decorators: [
(Story) => (
<StrictMode>
<Story />
</StrictMode>
),
],
};

export default preview;
11 changes: 11 additions & 0 deletions docs/react/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,20 @@ import { GraphCanvas } from "@gravity-ui/graph/react";
graph={graph}
renderBlock={renderBlock}
className="my-graph"
blockListClassName="custom-blocks-container"
reactLayerRef={reactLayerRef}
/>
```

#### Props

- **`graph`** (required): The Graph instance to render
- **`renderBlock`** (optional): Function to render custom block components
- **`className`** (optional): CSS class for the main container
- **`blockListClassName`** (optional): CSS class applied to the blocks container layer
- **`reactLayerRef`** (optional): Ref to access the ReactLayer instance directly
- **Event callbacks**: All graph event callbacks can be passed as props

### GraphBlock

The `GraphBlock` component is a crucial wrapper that handles the complex interaction between HTML elements and the canvas layer. It's responsible for:
Expand Down
3 changes: 2 additions & 1 deletion src/components/canvas/blocks/Blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

private font: string;

constructor(props: {}, context: any) {

Check warning on line 17 in src/components/canvas/blocks/Blocks.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected any. Specify a different type
super(props, context);

this.unsubscribe = this.subscribe();
Expand All @@ -27,8 +27,9 @@
}

protected rerender() {
this.performRender();
this.shouldRenderChildren = true;
this.shouldUpdateChildren = true;
this.performRender();
}

protected subscribe() {
Expand Down
13 changes: 0 additions & 13 deletions src/components/canvas/groups/BlockGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
constants: this.props.graph.graphConstants,
colors: this.props.graph.graphColors,
graph: this.props.graph,
ownerDocument: this.props.root!,

Check warning on line 93 in src/components/canvas/groups/BlockGroups.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Forbidden non-null assertion
});

this.unsubscribe.push(
Expand All @@ -104,19 +104,6 @@
this.performRender = this.performRender.bind(this);
}

/**
* Called after initialization and when the layer is reattached.
* This is where we set up event subscriptions to ensure they work properly
* after the layer is unmounted and reattached.
*/
protected afterInit(): void {
// Register event listener with the onGraphEvent method for automatic cleanup when unmounted
this.onGraphEvent("camera-change", this.performRender);

// Call parent afterInit to ensure proper initialization
super.afterInit();
}

public getParent(): CoreComponent | undefined {
/*
* Override parent to delegate click events to camera.
Expand Down
6 changes: 6 additions & 0 deletions src/components/canvas/layers/graphLayer/GraphLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ export class GraphLayer extends Layer<TGraphLayerProps, TGraphLayerContext> {

// Subscribe to graph events here instead of in the constructor
this.onGraphEvent("camera-change", this.performRender);
this.context.graph.rootStore.blocksList.$blocks.subscribe(() => {
this.performRender();
});
this.context.graph.rootStore.connectionsList.$connections.subscribe(() => {
this.performRender();
});
super.afterInit();
}

Expand Down
13 changes: 8 additions & 5 deletions src/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,13 @@ export class Graph {
this.setConstants(graphConstants);
}

this.layers.on("update-size", (event: IRect) => {
this.cameraService.set(event);
});

this.setupGraph(config);
}

protected onUpdateSize = (event: IRect) => {
this.cameraService.set(event);
};

public getGraphLayer() {
return this.graphLayer;
}
Expand Down Expand Up @@ -324,6 +324,7 @@ export class Graph {
if (this.state === GraphState.READY) {
return;
}
rootEl[Symbol.for("graph")] = this;
this.layers.attach(rootEl);

const { width: rootWidth, height: rootHeight } = this.layers.getRootSize();
Expand All @@ -349,6 +350,7 @@ export class Graph {
if (rootEl) {
this.attach(rootEl);
}
this.layers.on("update-size", this.onUpdateSize);
this.layers.start();
this.scheduler.start();
this.setGraphState(GraphState.READY);
Expand Down Expand Up @@ -395,9 +397,10 @@ export class Graph {

public unmount() {
this.detach();
this.layers.off("update-size", this.onUpdateSize);
this.setGraphState(GraphState.INIT);
this.hitTest.clear();
this.layers.destroy();
this.layers.unmount();
clearTextCache();
this.rootStore.reset();
this.scheduler.stop();
Expand Down
2 changes: 1 addition & 1 deletion src/graphConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export const initGraphConstants: TGraphConstants = {
system: {
GRID_SIZE: 16,
/* @deprecated this config is not used anymore, Layers checks devicePixelRatio internally */
PIXEL_RATIO: typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1,
PIXEL_RATIO: typeof globalThis !== "undefined" ? globalThis.devicePixelRatio || 1 : 1,
USABLE_RECT_GAP: 400,
CAMERA_VIEWPORT_TRESHOLD: 0.5,
},
Expand Down
95 changes: 22 additions & 73 deletions src/plugins/devtools/DevToolsLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ import "./devtools-layer.css"; // Import the CSS file after type imports
*/
export class DevToolsLayer extends Layer<TDevToolsLayerProps, LayerContext, TDevToolsLayerState> {
public state = INITIAL_DEVTOOLS_LAYER_STATE;
private mouseMoveListener = this.handleMouseMove.bind(this);
private mouseEnterListener = this.handleMouseEnter.bind(this);
private mouseLeaveListener = this.handleMouseLeave.bind(this);
protected cameraSubscription: (() => void) | null = null;

// HTML elements for ruler backgrounds
private horizontalRulerBgEl: HTMLDivElement | null = null;
Expand Down Expand Up @@ -77,22 +73,27 @@ export class DevToolsLayer extends Layer<TDevToolsLayerProps, LayerContext, TDev
}

protected afterInit(): void {
super.afterInit();

if (this.context.graph) {
this.cameraSubscription = this.context.graph.on("camera-change", () => this.performRender());
} else {
console.error("DevToolsLayer: Graph instance not found in context during afterInit.");
}

const graphRoot = this.props.graph?.layers?.$root;
if (graphRoot) {
graphRoot.addEventListener("mousemove", this.mouseMoveListener);
graphRoot.addEventListener("mouseenter", this.mouseEnterListener);
graphRoot.addEventListener("mouseleave", this.mouseLeaveListener);
} else {
console.error("DevToolsLayer: Graph root layer element ($root) not found.");
}
this.onGraphEvent("camera-change", () => this.performRender());
this.onRootEvent(
"mousemove",
(event: MouseEvent): void => {
const canvas = this.context.graphCanvas;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
this.setState({
mouseX: event.clientX - rect.left,
mouseY: event.clientY - rect.top,
isMouseInside: true,
});
},
{ capture: true }
);
this.onRootEvent("mouseenter", (): void => {
this.setState({ isMouseInside: true });
});
this.onRootEvent("mouseleave", (): void => {
this.setState({ isMouseInside: false, mouseX: null, mouseY: null });
});

// Create HTML elements for ruler backgrounds if showRuler is initially true
const htmlContainer = this.getHTML();
Expand Down Expand Up @@ -123,61 +124,9 @@ export class DevToolsLayer extends Layer<TDevToolsLayerProps, LayerContext, TDev
htmlContainer.appendChild(this.verticalRulerBgEl);
}

this.performRender(); // Initial render for positioning divs and canvas ticks/text
}

protected unmountLayer(): void {
this.cameraSubscription?.();
this.cameraSubscription = null;

const graphRoot = this.props.graph?.layers?.$root;
if (graphRoot) {
graphRoot.removeEventListener("mousemove", this.mouseMoveListener);
graphRoot.removeEventListener("mouseenter", this.mouseEnterListener);
graphRoot.removeEventListener("mouseleave", this.mouseLeaveListener);
}

// Remove ruler background elements
this.horizontalRulerBgEl?.remove();
this.verticalRulerBgEl?.remove();
this.horizontalRulerBgEl = null;
this.verticalRulerBgEl = null;

super.unmountLayer();
}

// --- Event Handlers ---

private handleMouseMove(event: MouseEvent): void {
const canvas = this.context.graphCanvas;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
this.setState({
mouseX: event.clientX - rect.left,
mouseY: event.clientY - rect.top,
isMouseInside: true,
});
}

private handleMouseEnter(): void {
this.setState({ isMouseInside: true });
}

private handleMouseLeave(): void {
this.setState({ isMouseInside: false, mouseX: null, mouseY: null });
}

// --- State Update ---

protected stateChanged(): void {
// Only re-render canvas if crosshair is visible and mouse position changed
if (this.props.showCrosshair) {
this.performRender();
}
super.afterInit();
}

// --- Rendering Logic ---

protected render(): void {
if (!this.context.ctx || !this.context.graphCanvas) {
return;
Expand Down
7 changes: 6 additions & 1 deletion src/plugins/minimap/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,16 @@ export class MiniMapLayer extends Layer<MiniMapLayerProps, MiniMapLayerContext>
this.onCanvasEvent("mousedown", this.handleMouseDownEvent);
}
}
this.onSignal(this.props.graph.hitTest.$usableRect, () => {
this.onBlockUpdated();
this.calculateViewPortCoords();
this.rerenderMapContent();
});

super.afterInit();
}

public updateSize(): void {
protected updateCanvasSize(): void {
this.rerenderMapContent();
}

Expand Down
41 changes: 15 additions & 26 deletions src/react-components/BlocksList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Graph, GraphState } from "../graph";
import { ESchedulerPriority } from "../lib";
import { ECameraScaleLevel, TCameraState } from "../services/camera/CameraService";
import { BlockState } from "../store/block/Block";
import { debounce, throttle } from "../utils/functions";
import { debounce } from "../utils/functions";

import { useSignal } from "./hooks";
import { useGraphEvent } from "./hooks/useGraphEvents";
Expand Down Expand Up @@ -96,41 +96,30 @@ export const BlocksList = memo(function BlocksList({ renderBlock, graphObject }:
setGraphState(graphObject.state);
}, [graphObject]);

const throttleUpdate = useMemo(
() =>
throttle(
({ scale }: TCameraState) => {
if (graphObject.cameraService.getCameraBlockScaleLevel(scale) !== ECameraScaleLevel.Detailed) {
setRenderAllowed(false);
return;
}
setRenderAllowed(true);
scheduleListUpdate();
if (!isRenderAllowed) {
scheduleListUpdate.flush();
}
},
{
priority: ESchedulerPriority.HIGHEST,
frameTimeout: 15,
}
),
[]
);

useGraphEvent(graphObject, "camera-change", throttleUpdate);
useGraphEvent(graphObject, "camera-change", ({ scale }: TCameraState) => {
if (graphObject.cameraService.getCameraBlockScaleLevel(scale) !== ECameraScaleLevel.Detailed) {
setRenderAllowed(false);
return;
}
setRenderAllowed(true);
scheduleListUpdate();
if (!isRenderAllowed) {
scheduleListUpdate.flush();
}
});

useEffect(() => {
return () => {
throttleUpdate.cancel();
// throttleUpdate.cancel();
scheduleListUpdate.cancel();
};
}, []);

// init list
useEffect(() => {
graphObject.hitTest.waitUsableRectUpdate(updateBlockList);
return graphObject.hitTest.onUsableRectUpdate(updateBlockList);
}, [graphObject.hitTest, throttleUpdate, isRenderAllowed, graphState]);
}, [graphObject.hitTest, isRenderAllowed, graphState]);

return (
<>
Expand Down
12 changes: 10 additions & 2 deletions src/react-components/GraphCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,21 @@ import { useFn } from "./utils/hooks/useFn";
export type GraphProps = Pick<Partial<TBlockListProps>, "renderBlock"> &
Partial<TGraphEventCallbacks> & {
className?: string;
blockListClassName?: string;
graph: Graph;
reactLayerRef?: React.MutableRefObject<ReactLayer | null>;
};

export function GraphCanvas({ graph, className, renderBlock, ...cbs }: GraphProps) {
export function GraphCanvas({ graph, className, blockListClassName, renderBlock, reactLayerRef, ...cbs }: GraphProps) {
const containerRef = useRef<HTMLDivElement>();

const reactLayer = useLayer(graph, ReactLayer, {});
const reactLayer = useLayer(graph, ReactLayer, {
blockListClassName,
});

if (reactLayerRef) {
reactLayerRef.current = reactLayer;
}

useEffect(() => {
if (containerRef.current) {
Expand Down
9 changes: 9 additions & 0 deletions src/react-components/hooks/useGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ export function useGraph(config: HookGraphParams) {
});
}, [config.graph]);

// Cleanup graph on unmount to prevent memory leaks in strict mode
useLayoutEffect(() => {
return () => {
if (!config.graph && graph) {
graph.unmount();
}
};
}, [graph]);

const setViewConfiguration = useFn((viewConfig: HookGraphParams["viewConfiguration"]) => {
if (viewConfig.colors) {
graph.setColors(config.viewConfiguration.colors);
Expand Down
Loading
Loading