Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom connections #20

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ npm install @gravity-ui/graph
- [Customization connection](https://preview.gravity-ui.com/graph/?path=/story/api-updateconnection--default)

```jsx
import { GraphCanvas, GraphState, TRenderBlockFn, useGraph } from "@gravity-ui/graph";
import { GraphCanvas, GraphState, TRenderBlockFn, GraphBlock, useGraph } from "@gravity-ui/graph";
import React from "react";

const config = {};
Expand Down Expand Up @@ -62,8 +62,8 @@ export function GraphEditor() {
})
})

const renderBlock = (graph, block) => {
return <HTMLBlockView graph={graph} block={block} />;
const renderBlockFn = (graph, block) => {
return <GraphBlock graph={graph} block={block}>{block.id}</GraphBlock>;
};

return (
Expand All @@ -86,4 +86,3 @@ export function GraphEditor() {
- [Pulic API](docs/public_api.md)
- [Graph Events](docs/events.md)
- [Editing](docs/editing.md)

73 changes: 40 additions & 33 deletions src/components/canvas/connections/BlockConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Block } from "../blocks/Block";
import { GraphLayer, TGraphLayerContext } from "../layers/graphLayer/GraphLayer";

import withBatchedConnection from "./batchMixins/withBatchedConnection";
import { TConnectionRenderSettings } from "./batchMixins/withBatchedConnections";
import { bezierCurveLine, generateBezierParams, getArrowCoords, isPointInStroke } from "./bezierHelpers";
import { getLabelCoords } from "./labelHelper";

Expand Down Expand Up @@ -51,19 +52,19 @@ export class BlockConnection extends withBatchedConnection(withHitTest(EventedCo

protected readonly unsubscribe: (() => void)[];

private sourceBlock: Block;
public sourceBlock: Block;

private sourceAnchor?: TAnchor;
public sourceAnchor?: TAnchor;

private targetBlock: Block;
public targetBlock: Block;

private targetAnchor?: TAnchor;
public targetAnchor?: TAnchor;

private hitBoxHash = 0;
public hitBoxHash = 0;

protected path2d?: Path2D;
public path2d?: Path2D;

private geometry = { x1: 0, x2: 0, y1: 0, y2: 0 };
public geometry = { x1: 0, x2: 0, y1: 0, y2: 0 };

private labelGeometry = { x: 0, y: 0, width: 0, height: 0 };

Expand Down Expand Up @@ -104,10 +105,13 @@ export class BlockConnection extends withBatchedConnection(withHitTest(EventedCo
this.sourceBlock = this.connectedState.$sourceBlock.value?.getViewComponent();
this.targetBlock = this.connectedState.$targetBlock.value?.getViewComponent();

if (this.connectedState.sourceAnchorId && this.connectedState.targetAnchorId) {
if (this.connectedState.sourceAnchorId) {
this.sourceAnchor = this.sourceBlock.connectedState
.getAnchorById(this.connectedState.sourceAnchorId)
?.asTAnchor();
}

if (this.connectedState.targetAnchorId) {
this.targetAnchor = this.targetBlock.connectedState
.getAnchorById(this.connectedState.targetAnchorId)
?.asTAnchor();
Expand Down Expand Up @@ -136,18 +140,17 @@ export class BlockConnection extends withBatchedConnection(withHitTest(EventedCo
return;
}

this.shouldRender = this.context.camera.isLineVisible(
this.geometry.x1,
this.geometry.y1,
this.geometry.x2,
this.geometry.y2
);
this.shouldRender = this.isConnectionVisible();
}

public computeRenderSettings(props, state: TConnectionState) {
public isConnectionVisible() {
return this.context.camera.isLineVisible(this.geometry.x1, this.geometry.y1, this.geometry.x2, this.geometry.y2);
}

public computeRenderSettings(props, state: TConnectionState): TConnectionRenderSettings {
let lineWidth = 2;
let zIndex = 1;
let lineDash: number[] | boolean = false;
let lineDash: number[] | undefined;
const strokeStyle = this.getStrokeColor(state);

if (state.selected) {
Expand Down Expand Up @@ -180,14 +183,18 @@ export class BlockConnection extends withBatchedConnection(withHitTest(EventedCo
protected didRender() {
super.didRender();

const hash = this.geometry.x1 + this.geometry.y1 + this.geometry.x2 + this.geometry.y2;
const hash = this.calculateHitBoxHash();

if (this.hitBoxHash !== hash) {
this.hitBoxHash = hash;
this.updateHitBox();
}
}

public calculateHitBoxHash() {
return this.geometry.x1 + this.geometry.y1 + this.geometry.x2 + this.geometry.y2;
}

public handleEvent(event: MouseEvent | KeyboardEvent) {
event.stopPropagation();

Expand All @@ -208,7 +215,7 @@ export class BlockConnection extends withBatchedConnection(withHitTest(EventedCo
}
}

private updateHitBox = () => {
public updateHitBox = () => {
const labelGeometry = this.labelGeometry;
let minX: number;
let maxX: number;
Expand Down Expand Up @@ -254,7 +261,7 @@ export class BlockConnection extends withBatchedConnection(withHitTest(EventedCo

public onHitBox(shape: HitBoxData): boolean {
const THRESHOLD_LINE_HIT = this.context.constants.connection.THRESHOLD_LINE_HIT;
const relativeTreshold = THRESHOLD_LINE_HIT / this.context.camera.getCameraScale();
const relativeThreshold = THRESHOLD_LINE_HIT / this.context.camera.getCameraScale();
const x = (shape.minX + shape.maxX) / 2;
const y = (shape.minY + shape.maxY) / 2;

Expand All @@ -264,25 +271,25 @@ export class BlockConnection extends withBatchedConnection(withHitTest(EventedCo
this.props.useBezier && this.path2d
? isPointInStroke(this.context.ctx, this.path2d, shape.x, shape.y, THRESHOLD_LINE_HIT * 2)
: intersects.boxLine(
x - relativeTreshold / 2,
y - relativeTreshold / 2,
relativeTreshold,
relativeTreshold,
this.geometry.x1,
this.geometry.y1,
this.geometry.x2,
this.geometry.y2
);
x - relativeThreshold / 2,
y - relativeThreshold / 2,
relativeThreshold,
relativeThreshold,
this.geometry.x1,
this.geometry.y1,
this.geometry.x2,
this.geometry.y2
);

if (this.labelGeometry !== undefined) {
return (
superHit &&
(intersectsLine ||
intersects.boxBox(
x - relativeTreshold / 2,
y - relativeTreshold / 2,
relativeTreshold,
relativeTreshold,
x - relativeThreshold / 2,
y - relativeThreshold / 2,
relativeThreshold,
relativeThreshold,
this.labelGeometry.x,
this.labelGeometry.y,
this.labelGeometry.width,
Expand All @@ -294,7 +301,7 @@ export class BlockConnection extends withBatchedConnection(withHitTest(EventedCo
}
}

private updateGeometry(sourceBlock: Block, targetBlock: Block) {
public updateGeometry(sourceBlock: Block, targetBlock: Block) {
if (!sourceBlock || !targetBlock) return;
const scale = this.context.camera.getCameraScale();
const isSchematicView = scale < this.context.constants.connection.SCALES[1];
Expand Down
12 changes: 10 additions & 2 deletions src/components/canvas/connections/BlockConnections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export class BlockConnections extends withBatchedConnections(Component) {

public removeFromRenderOrder = this.removeFromRenderOrder.bind(this);

protected connectionsView = {};

constructor(props: {}, parent: GraphLayer) {
super(props, parent);
this.unsubscribe = this.subscribe();
Expand All @@ -28,6 +30,7 @@ export class BlockConnections extends withBatchedConnections(Component) {

protected subscribe() {
this.connections = this.context.graph.rootStore.connectionsList.$connections.value;
this.connectionsView = this.context.graph.rootStore.settings.getConfigFlag("connectionComponents");

const r1 = this.context.graph.rootStore.settings.$connectionsSettings.subscribe(() => {
this.scheduleUpdate();
Expand All @@ -38,7 +41,12 @@ export class BlockConnections extends withBatchedConnections(Component) {
this.scheduleUpdate();
});

return [r1, r2];
const r3 = this.context.graph.rootStore.settings.$connectionComponents.subscribe((connectionsView) => {
this.connectionsView = connectionsView;
this.scheduleUpdate();
});

return [r1, r2, r3];
}

protected unmount() {
Expand All @@ -60,7 +68,7 @@ export class BlockConnections extends withBatchedConnections(Component) {
showConnectionLabels: settings.showConnectionLabels,
showConnectionArrows: settings.showConnectionArrows,
};
return BlockConnection.create(props, { key: String(connection.id) });
return (this.connectionsView[connection.$state.value.is] || BlockConnection).create(props, { key: String(connection.id) });
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { TGraphLayerContext } from "../../layers/graphLayer/GraphLayer";
import { IWithBatchedConnection } from "./withBatchedConnection";

export interface IWithBatchedConnections {
addInRenderOrder(comp: unknown, settings: object): void;
addInRenderOrder(comp: unknown, settings: TConnectionRenderSettings): void;
removeFromRenderOrder(comp: unknown): void;
}

type ConnectionComponent = Component & IWithBatchedConnection;
type StyleSettings = object & { zIndex: number };
export type TConnectionRenderSettings = Partial<
Pick<CanvasRenderingContext2D, "fillStyle" | "lineWidth" | "strokeStyle">
> & { zIndex: number; lineDash?: number[] };

// default export because https://github.com/microsoft/TypeScript/issues/30355#issuecomment-671095933
export default <T extends Constructor<Component>>(superclass: T): T & Constructor<IWithBatchedConnections> =>
Expand All @@ -18,15 +20,15 @@ export default <T extends Constructor<Component>>(superclass: T): T & Constructo

private mapKeyToComps = new Map<string, Set<ConnectionComponent>>();

private mapKeyToSettings = new Map<string, StyleSettings>();
private mapKeyToSettings = new Map<string, TConnectionRenderSettings>();

private mapCompToComps = new WeakMap<ConnectionComponent, Set<ConnectionComponent>>();

private wasModifyed = false;
private wasModified = false;

private sortedCouplesKeySetting: [string, StyleSettings][] = [];
private sortedCouplesKeySetting: [string, TConnectionRenderSettings][] = [];

public addInRenderOrder(comp: ConnectionComponent, settings: StyleSettings) {
public addInRenderOrder(comp: ConnectionComponent, settings: TConnectionRenderSettings) {
const key = JSON.stringify(settings);
let comps = this.mapCompToComps.get(comp);

Expand All @@ -46,7 +48,7 @@ export default <T extends Constructor<Component>>(superclass: T): T & Constructo
}

this.mapCompToComps.set(comp, comps);
this.wasModifyed = true;
this.wasModified = true;
this.performRender();
}

Expand All @@ -61,40 +63,35 @@ export default <T extends Constructor<Component>>(superclass: T): T & Constructo
protected willIterate() {
super.willIterate();

if (this.wasModifyed) {
this.wasModifyed = false;
if (this.wasModified) {
this.wasModified = false;
this.sortedCouplesKeySetting = Array.from(this.mapKeyToSettings.entries()).sort((item1, item2) => {
return item2[1].zIndex - item1[1].zIndex;
});
}
}

protected render() {
let i;
let j;
let key;
let settings;
let comps;

for (i = 0; i < this.sortedCouplesKeySetting.length; i += 1) {
key = this.sortedCouplesKeySetting[i][0];
settings = this.sortedCouplesKeySetting[i][1];
for (let i = 0; i < this.sortedCouplesKeySetting.length; i += 1) {
const key = this.sortedCouplesKeySetting[i][0];
const settings = this.sortedCouplesKeySetting[i][1];

// because original will be changed during addInRenderOrder while ._iterate() below
comps = [...this.mapKeyToComps.get(key)];
const comps = [...this.mapKeyToComps.get(key)];

this.context.ctx.beginPath();

for (j = 0; j < comps.length; j += 1) {
for (let j = 0; j < comps.length; j += 1) {
comps[j]._iterate();
}

if ("lineDash" in settings && settings.lineDash) {
this.context.ctx.setLineDash(settings.lineDash);
}

Object.assign(this.context.ctx, settings);
this.context.ctx.stroke();

if (settings.strokeStyle) this.context.ctx.stroke();
if (settings.fillStyle) this.context.ctx.fill();

if ("lineDash" in settings) {
this.context.ctx.setLineDash([]);
Expand Down
11 changes: 10 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
export { Anchor, type TAnchor } from "./components/canvas/anchors";
export { Block as CanvasBlock, type TBlock } from "./components/canvas/blocks/Block";
export { Block as CanvasBlock, type TBlock, type TBlockProps } from "./components/canvas/blocks/Block";
export { generateBezierParams, isPointInStroke } from "./components/canvas/connections/bezierHelpers";
export {
BlockConnection as CanvasConnection,
type TConnectionProps,
} from "./components/canvas/connections/BlockConnection";
export { type GraphLayer } from "./components/canvas/layers/graphLayer/GraphLayer";
export * from "./graph";
export type { TGraphColors, TGraphConstants } from "./graphConfig";
export * from "./plugins";
export * from "./react-component";
export { ECameraScaleLevel } from "./services/camera/CameraService";
export { HitBoxData } from "./services/HitTest";
export * from "./services/Layer";
export * from "./store";
export { EAnchorType } from "./store/anchor/Anchor";
export type { BlockState, TBlockId } from "./store/block/Block";
export type { ConnectionState, TConnection, TConnectionId } from "./store/connection/ConnectionState";
export { ECanChangeBlockGeometry } from "./store/settings";
export { addEventListeners } from "./utils/functions";
export { dragListener } from "./utils/functions/dragListener";
export * from "./utils/renderers/text";
export { EVENTS } from "./utils/types/events";
export { type TPoint, type TRect } from "./utils/types/shapes";
Expand Down
2 changes: 1 addition & 1 deletion src/react-component/Block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const GraphBlock = <T extends TBlock>({
}: {
graph: Graph;
block: T;
children: React.ReactNode;
children?: React.ReactNode;
className?: string;
containerClassName?: string;
}) => {
Expand Down
5 changes: 3 additions & 2 deletions src/react-component/hooks/useGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { batch } from "@preact/signals-core";

import { ZoomConfig } from "../../api/PublicGraphApi";
import type { TBlock } from "../../components/canvas/blocks/Block";
import { Graph, GraphState, TGraphConfig } from "../../graph";
import type { TGraphZoomTarget } from "../../graph";
import { Graph, GraphState, TGraphConfig } from "../../graph";
import type { TGraphColors, TGraphConstants } from "../../graphConfig";
import type { Layer } from "../../services/Layer";
import type { TConnection } from "../../store/connection/ConnectionState";
import { TGraphSettingsConfig } from "../../store/settings";
import { useFn } from "../../utils/hooks/useFn";
import { RecursivePartial } from "../../utils/types/helpers";

Expand Down Expand Up @@ -57,7 +58,7 @@ export function useGraph(config: HookGraphParams) {
return {
graph,
api: graph.api,
setSettings: useFn((settings) => graph.updateSettings(settings)),
setSettings: useFn((settings: Partial<TGraphSettingsConfig>) => graph.updateSettings(settings)),
start: useFn(() => {
if (graph.state !== GraphState.READY) {
graph.start();
Expand Down
1 change: 1 addition & 0 deletions src/store/connection/ConnectionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type TConnectionId = string | number | symbol;

export type TConnection = {
id?: TConnectionId;
is?: string;
sourceBlockId: TBlockId;
targetBlockId: TBlockId;
sourceAnchorId?: string;
Expand Down
1 change: 1 addition & 0 deletions src/store/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe("Settings store", () => {
connectivityComponentOnClickRaise: true,
showConnectionLabels: false,
blockComponents: {},
connectionComponents: {},
});
});
it("Should get config via key", () => {
Expand Down
Loading
Loading