Skip to content

Commit

Permalink
feat: elk converter
Browse files Browse the repository at this point in the history
  • Loading branch information
SimbiozizV committed Dec 26, 2024
1 parent 26c3724 commit 9b929c3
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 150 deletions.
94 changes: 94 additions & 0 deletions src/components/canvas/connections/MultipointConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Path2DRenderStyleResult } from "../../../components/canvas/connections/BatchPath2D";
import { BlockConnection } from "../../../components/canvas/connections/BlockConnection";
import { TMultipointConnection } from "../../../store/connection/ConnectionState";
import { curvePolyline } from "../../../utils/shapes/curvePolyline";
import { trangleArrowForVector } from "../../../utils/shapes/triangle";

const DEFAULT_FONT_SIZE = 14;

export class MultipointConnection extends BlockConnection<TMultipointConnection> {
public createPath() {
const { points } = this.connectedState.$state.value;
if (!points.length) {
return super.createPath();
}
return curvePolyline(points, 10);
}

public createArrowPath(): Path2D {
const { points } = this.connectedState.$state.value;
if (!points.length) {
return undefined;
}

const [start, end] = points.slice(points.length - 2);
return trangleArrowForVector(start, end, 16, 10);
}

public styleArrow(ctx: CanvasRenderingContext2D): Path2DRenderStyleResult {
ctx.fillStyle = this.state.selected
? this.context.colors.connection.selectedBackground
: this.context.colors.connection.background;
ctx.strokeStyle = ctx.fillStyle;
ctx.lineWidth = this.state.selected || this.state.hovered ? -1 : 1;
return { type: "both" };
}

public getPoints() {
return this.connectedState.$state.value.points || [];
}

public afterRender?(ctx: CanvasRenderingContext2D): void {
this.renderLabelsText(ctx);
}

public updatePoints(): void {
super.updatePoints();
return;
}

public getBBox() {
const { points } = this.connectedState.$state.value;
if (!points.length) {
return super.getBBox();
}

const x = [];
const y = [];
points.forEach((point) => {
x.push(point.x);
y.push(point.y);
});

return [Math.min(...x), Math.min(...y), Math.max(...x), Math.max(...y)] as const;
}

private renderLabelsText(ctx: CanvasRenderingContext2D) {
const { labels } = this.connectedState.$state.value;
if (!labels || !labels.length) {
return;
}

labels.forEach(({ x, y, text, height }) => {
if ([x, y, text].some((i) => i === undefined)) {
return;
}
ctx.fillStyle = this.context.colors.connectionLabel.text;

if (this.state.hovered) ctx.fillStyle = this.context.colors.connectionLabel.hoverText;
if (this.state.selected) ctx.fillStyle = this.context.colors.connectionLabel.selectedText;

ctx.textBaseline = "top";
ctx.textAlign = "center";
ctx.font = `${height || DEFAULT_FONT_SIZE}px sans-serif`;
ctx.fillText(text, x, y);

ctx.fillStyle = this.context.colors.connectionLabel.background;

if (this.state.hovered) ctx.fillStyle = this.context.colors.connectionLabel.hoverBackground;
if (this.state.selected) ctx.fillStyle = this.context.colors.connectionLabel.selectedBackground;
});

return;
}
}
40 changes: 40 additions & 0 deletions src/react-component/hooks/useElk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useCallback, useEffect, useMemo, useState } from "react";

import ELK, { ElkLayoutArguments, ElkNode } from "elkjs";

import { elkConverter } from "../../utils/converters/eklConverter";
import { ConverterResult } from "../../utils/types/types";

export const useElk = (config: ElkNode, args?: ElkLayoutArguments) => {
const [result, setResult] = useState<ConverterResult | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);

const elk = useMemo(() => new ELK(), []);

const layout = useCallback(() => {
return elk.layout(config, args);
}, [elk, config, args]);

useEffect(() => {
let isCancelled = false;

layout()
.then((data) => {
if (isCancelled) return;
setResult(elkConverter(data));
setIsLoading(false);
})
.catch((error) => {
if (!isCancelled) {
console.error("Error during ELK layout:", error);
setIsLoading(false);
}
});

return () => {
isCancelled = true;
};
}, [layout]);

return { result, isLoading, elk };
};
14 changes: 14 additions & 0 deletions src/store/connection/ConnectionState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { computed, signal } from "@preact/signals-core";

import { TConnectionColors } from "../../graphConfig";
import { TPoint } from "../../utils/types/shapes";
import { ESelectionStrategy } from "../../utils/types/types";
import { TBlockId } from "../block/Block";

Expand All @@ -24,6 +25,19 @@ export type TConnection = {
selected?: boolean;
};

export type TLabel = {
height?: number;
width?: number;
x?: number;
y?: number;
text?: string;
};

export type TMultipointConnection = TConnection & {
points?: TPoint[];
labels?: TLabel[];
};

export class ConnectionState<T extends TConnection = TConnection> {
public $state = signal<T>(undefined);

Expand Down
82 changes: 0 additions & 82 deletions src/stories/examples/elk/ELKConnection.ts

This file was deleted.

10 changes: 10 additions & 0 deletions src/stories/examples/elk/calculateTextDimensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const calculateTextDimensions = (text: string, fontSize: number, fontFamily: string) => {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d")!;
context.font = `${fontSize}px ${fontFamily}`;
const metrics = context.measureText(text);
return {
width: metrics.width,
height: fontSize,
};
};
104 changes: 36 additions & 68 deletions src/stories/examples/elk/elk.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,96 +2,64 @@ import React, { useEffect, useMemo, useState } from "react";

import { Select, SelectOption, ThemeProvider } from "@gravity-ui/uikit";
import type { Meta, StoryFn } from "@storybook/react";
import ELK, { ElkExtendedEdge, ElkNode } from "elkjs";

import { MultipointConnection } from "../../../components/canvas/connections/MultipointConnection";
import { Graph, GraphCanvas, GraphState, TBlock, useGraph, useGraphEvent } from "../../../index";
import { useElk } from "../../../react-component/hooks/useElk";
import { useFn } from "../../../utils/hooks/useFn";
import { generatePrettyBlocks } from "../../configurations/generatePretty";
import { BlockStory } from "../../main/Block";

import { ELKConnection } from "./ELKConnection";
import { getElkConfig } from "./getElkConfig";

import "@gravity-ui/uikit/styles/styles.css";

export type TElkBlock = TBlock & {
elk: ElkNode;
};

const config = generatePrettyBlocks(10, 30, true);

const GraphApp = () => {
const [algorithm, setAlgorithm] = useState("layered");
const { graph, setEntities, start } = useGraph({
settings: {
connection: ELKConnection,
connection: MultipointConnection,
},
});

const elk = useMemo(() => new ELK({}), []);
const elkConfig = useMemo(() => {
return getElkConfig(config, algorithm);
}, [algorithm]);

const [algoritm, setAlgortm] = useState("layered");
const { isLoading, elk, result } = useElk(elkConfig);

useEffect(() => {
const { blocks, connections } = config;
const blocksMap = new Map(blocks.map((b) => [b.id, b]));
const conMap = new Map(connections.map((b) => [b.id, b]));

const graphDefinition = {
id: "root",
layoutOptions: { "elk.algorithm": algoritm, "elk.spacing.edgeNode": "500.0", "elk.spacing.nodeNode": "500.0" },
children: blocks.map((b) => {
return {
id: b.id as string,
width: b.width,
height: b.height,
} satisfies ElkNode;
}),
edges: connections.map((c) => {
return {
id: c.id as string,
sources: [c.sourceBlockId as string],
targets: [c.targetBlockId as string],
} satisfies ElkExtendedEdge;
}),
};

elk
.layout(graphDefinition)
.then((result) => {
const { children, edges } = result;

const con = edges.map((edge) => {
const c = conMap.get(edge.id);
return {
...c,
elk: edge,
};
});
const layoutedBlocks = children.map((child) => {
const b = blocksMap.get(child.id);

return {
...b,
x: child.x,
y: child.y,
elk: child,
};
});

setEntities({
blocks: layoutedBlocks,
connections: con,
});

graph.zoomTo("center", { padding: 300 });
})
.catch(console.error);
}, [algoritm, elk]);

const [algorithms, setAlgortms] = useState<SelectOption[]>([]);
if (isLoading || !result) return;

const connections = config.connections.map((connection) => {
return {
...connection,
...result.edges[connection.id],
};
});

const blocks = config.blocks.map((block) => {
return {
...block,
...result.blocks[block.id],
};
});

setEntities({
blocks,
connections,
});

graph.zoomTo("center", { padding: 300 });
}, [isLoading, result]);

const [algorithms, setAlgorithms] = useState<SelectOption[]>([]);

useEffect(() => {
elk.knownLayoutAlgorithms().then((knownLayoutAlgorithms) => {
setAlgortms(
setAlgorithms(
knownLayoutAlgorithms.map((knownLayoutAlgorithm) => {
const { id, name } = knownLayoutAlgorithm;
const algId = id.split(".").at(-1);
Expand All @@ -113,7 +81,7 @@ const GraphApp = () => {

return (
<ThemeProvider theme={"light"}>
<Select value={[algoritm]} options={algorithms} onUpdate={(v) => setAlgortm(v[0])}></Select>
<Select value={[algorithm]} options={algorithms} onUpdate={(v) => setAlgorithm(v[0])}></Select>
<GraphCanvas className="graph" graph={graph} renderBlock={renderBlockFn} />;
</ThemeProvider>
);
Expand Down
Loading

0 comments on commit 9b929c3

Please sign in to comment.