From c2e4cd24af4102d787a2f9e289dcb706be74b3d6 Mon Sep 17 00:00:00 2001 From: draedful Date: Tue, 8 Oct 2024 21:32:32 +0300 Subject: [PATCH 01/20] story(playground): Add gravity playground. Add some fixes and features for playground --- .storybook/styles/global.css | 42 +++++- package-lock.json | 43 +++++- package.json | 1 + src/components/canvas/anchors/index.ts | 7 + src/components/canvas/blocks/Block.ts | 1 + .../canvas/connections/BlockConnection.ts | 2 + .../canvas/layers/graphLayer/GraphLayer.ts | 5 + src/graph.ts | 6 +- src/mixins/withEvents.ts | 127 ++++++++-------- src/react-component/Anchor.css | 1 + src/services/camera/Camera.ts | 5 +- .../newConnection/ConnectionService.ts | 49 +++++- src/services/newConnection/NewConnection.ts | 21 ++- src/services/selection/SelectionArea.ts | 24 +-- src/store/settings.ts | 4 +- src/stories/Playground/Editor/Editor.css | 8 + src/stories/Playground/Editor/index.tsx | 72 +++++++++ src/stories/Playground/Editor/theme.ts | 58 ++++++++ src/stories/Playground/GraphPlayground.tsx | 133 +++++++++++++++++ .../Playground/GravityBlock/GravityBlock.css | 20 +++ .../Playground/GravityBlock/GravityBlock.tsx | 32 ++++ src/stories/Playground/GravityBlock/index.tsx | 140 ++++++++++++++++++ src/stories/Playground/Playground.css | 25 ++++ src/stories/Playground/Playground.stories.tsx | 13 ++ src/stories/Playground/generateLayout.tsx | 104 +++++++++++++ src/stories/configurations/generatePretty.ts | 4 +- src/utils/renderers/svgPath.ts | 20 +++ src/utils/renderers/text.ts | 22 ++- 28 files changed, 886 insertions(+), 103 deletions(-) create mode 100644 src/stories/Playground/Editor/Editor.css create mode 100644 src/stories/Playground/Editor/index.tsx create mode 100644 src/stories/Playground/Editor/theme.ts create mode 100644 src/stories/Playground/GraphPlayground.tsx create mode 100644 src/stories/Playground/GravityBlock/GravityBlock.css create mode 100644 src/stories/Playground/GravityBlock/GravityBlock.tsx create mode 100644 src/stories/Playground/GravityBlock/index.tsx create mode 100644 src/stories/Playground/Playground.css create mode 100644 src/stories/Playground/Playground.stories.tsx create mode 100644 src/stories/Playground/generateLayout.tsx create mode 100644 src/utils/renderers/svgPath.ts diff --git a/.storybook/styles/global.css b/.storybook/styles/global.css index 861c7de..ef0217c 100644 --- a/.storybook/styles/global.css +++ b/.storybook/styles/global.css @@ -1,7 +1,47 @@ +@mixin nv-legacy-text { + @font-face { + font-family: "YS Text"; + src: url("https://yastatic.net/s3/home/fonts/ys/1/text-light.woff2") format("woff2"); + font-weight: 300; + font-style: normal; + font-stretch: normal; + font-display: swap; + } + + @font-face { + font-family: "YS Text"; + src: url("https://yastatic.net/s3/home/fonts/ys/1/text-regular.woff2") format("woff2"); + font-weight: 400; + font-style: normal; + font-stretch: normal; + font-display: swap; + } + + @font-face { + font-family: "YS Text"; + src: url("https://yastatic.net/s3/home/fonts/ys/1/text-medium.woff2") format("woff2"); + font-weight: 500; + font-style: normal; + font-stretch: normal; + font-display: swap; + } + + @font-face { + font-family: "YS Text"; + src: url("https://yastatic.net/s3/home/fonts/ys/1/text-bold.woff2") format("woff2"); + font-weight: bold; + font-style: normal; + font-stretch: normal; + font-display: swap; + } +} + + .sb-show-main { padding: 0 !important; - margin: 0; + margin: 0; + font-family: "YS Text"; } .toolbox { diff --git a/package-lock.json b/package-lock.json index 38f932f..cb781f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "MIT", "dependencies": { + "@monaco-editor/react": "^4.6.0", "@preact/signals-core": "^1.5.1", "intersects": "^2.7.2", "lodash-es": "^4.17.21", @@ -4029,6 +4030,30 @@ "react": ">=16" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -13984,8 +14009,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -14440,7 +14464,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -14774,6 +14797,12 @@ "node": ">=0.4.0" } }, + "node_modules/monaco-editor": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", + "peer": true + }, "node_modules/moo-color": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz", @@ -16216,7 +16245,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -16315,7 +16343,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -16974,7 +17001,6 @@ "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -17301,6 +17327,11 @@ "node": ">=8" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/package.json b/package.json index 87c3635..de49b20 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ } ], "dependencies": { + "@monaco-editor/react": "^4.6.0", "@preact/signals-core": "^1.5.1", "intersects": "^2.7.2", "lodash-es": "^4.17.21", diff --git a/src/components/canvas/anchors/index.ts b/src/components/canvas/anchors/index.ts index 81a9f6e..53485bc 100644 --- a/src/components/canvas/anchors/index.ts +++ b/src/components/canvas/anchors/index.ts @@ -30,6 +30,9 @@ type TAnchorState = { }; export class Anchor extends withHitTest(EventedComponent) { + + public readonly cursor = 'pointer'; + public get zIndex() { // @ts-ignore this.__comp.parent instanceOf Block return this.__comp.parent.zIndex + 1; @@ -75,6 +78,10 @@ export class Anchor extends withHitTest(EventedComponent) { this.shift = this.props.size / 2 + props.lineWidth; } + public getPosition() { + return this.props.getPosition(this.props); + } + protected subscribe() { return [ this.connectedState.$selected.subscribe((selected) => { diff --git a/src/components/canvas/blocks/Block.ts b/src/components/canvas/blocks/Block.ts index deb8777..fb6fcbd 100644 --- a/src/components/canvas/blocks/Block.ts +++ b/src/components/canvas/blocks/Block.ts @@ -249,6 +249,7 @@ export class Block { private onRootPointerMove(event: MouseEvent) { if (this.targetComponent !== this.prevTargetComponent) { + if(this.targetComponent?.cursor) { + this.root.style.cursor = this.targetComponent?.cursor; + } else { + this.root.style.removeProperty('cursor'); + } this.applyEventToTargetComponent( new CustomEvent("mouseleave", { bubbles: false, diff --git a/src/graph.ts b/src/graph.ts index fefa970..d75e9c4 100644 --- a/src/graph.ts +++ b/src/graph.ts @@ -29,14 +29,14 @@ export type LayerConfig = Constructor> = [ ? Omit & { root?: Props["root"] } : never, ]; -export type TGraphConfig = {}> = { +export type TGraphConfig = { configurationName?: string; - blocks?: TBlock[]; + blocks?: B[]; connections?: TConnection[]; rect?: TRect; cameraXY?: TPoint; cameraScale?: number; - settings?: Partial>; + settings?: Partial>; layers?: LayerConfig[]; }; diff --git a/src/mixins/withEvents.ts b/src/mixins/withEvents.ts index 1b29c27..4f0f8e6 100644 --- a/src/mixins/withEvents.ts +++ b/src/mixins/withEvents.ts @@ -1,87 +1,86 @@ import { Component } from "../lib/Component"; import { Constructor } from "../lib/tshelpers"; -export function withEvent>(Base: T): T & Constructor { - return class WithEvent extends Base implements IWithEvent { - private _listenEvents: object = {}; +export class EventedComponent extends Component { + private _listenEvents: object = {}; - public unmount() { - super.unmount(); - this._listenEvents = {}; - } + public readonly cursor?: string; - public hasEventListener(type: string) { - return Object.prototype.hasOwnProperty.call(this._listenEvents, type); - } + protected unmount() { + super.unmount(); + this._listenEvents = {}; + } - public addEventListener(type: string, cbOrObject: any) { - if (Array.isArray(this._listenEvents[type])) { - this._listenEvents[type].push(cbOrObject); - } else { - this._listenEvents[type] = [cbOrObject]; - } + public hasEventListener(type: string) { + return Object.prototype.hasOwnProperty.call(this._listenEvents, type); + } + + public addEventListener(type: string, cbOrObject: any) { + if (Array.isArray(this._listenEvents[type])) { + this._listenEvents[type].push(cbOrObject); + } else { + this._listenEvents[type] = [cbOrObject]; } + return () => this.removeEventListener(type, cbOrObject); + } - public removeEventListener(type: string, cbOrObject: any) { - if (Array.isArray(this._listenEvents[type])) { - const i = this._listenEvents[type].indexOf(cbOrObject); + public removeEventListener(type: string, cbOrObject: any) { + if (Array.isArray(this._listenEvents[type])) { + const i = this._listenEvents[type].indexOf(cbOrObject); - if (i !== -1) { - this._listenEvents[type].splice(i, 1); - } + if (i !== -1) { + this._listenEvents[type].splice(i, 1); } } + } - public _fireEvent(cmp: any, event: Event) { - if (!this._hasListener(cmp, event.type)) { - return; - } + public _fireEvent(cmp: any, event: Event) { + if (!this._hasListener(cmp, event.type)) { + return; + } - const fnsOrObjects = cmp._listenEvents[event.type]; + const fnsOrObjects = cmp._listenEvents[event.type]; - for (let i = 0; i < fnsOrObjects.length; i += 1) { - if (typeof fnsOrObjects[i] === "function") { - fnsOrObjects[i](event); - } else if (typeof fnsOrObjects[i] === "object" && typeof fnsOrObjects[i].handleEvent === "function") { - fnsOrObjects[i].handleEvent(event); - } + for (let i = 0; i < fnsOrObjects.length; i += 1) { + if (typeof fnsOrObjects[i] === "function") { + fnsOrObjects[i](event); + } else if (typeof fnsOrObjects[i] === "object" && typeof fnsOrObjects[i].handleEvent === "function") { + fnsOrObjects[i].handleEvent(event); } } + } - public dispatchEvent(event: Event): boolean { - const bubbles = event.bubbles || false; + public dispatchEvent(event: Event): boolean { + const bubbles = event.bubbles || false; - if (bubbles) { - return this._dipping(this, event); - } else if (this._hasListener(this, event.type)) { - this._fireEvent(this, event); - return false; - } + if (bubbles) { + return this._dipping(this, event); + } else if (this._hasListener(this, event.type)) { + this._fireEvent(this, event); return false; } + return false; + } + + public _dipping(startParent: Component, event: Event) { + let stopPropagation = false; + let parent: Component | undefined = startParent; + event.stopPropagation = () => { + stopPropagation = true; + }; + + do { + this._fireEvent(parent, event); + if (stopPropagation) { + return false; + } + parent = parent.getParent() as Component; + } while (parent); - public _dipping(startParent: Component, event: Event) { - let stopPropagation = false; - let parent: Component | undefined = startParent; - event.stopPropagation = () => { - stopPropagation = true; - }; - - do { - this._fireEvent(parent, event); - if (stopPropagation) { - return false; - } - parent = parent.getParent() as Component; - } while (parent); - - return true; - } + return true; + } - public _hasListener(comp: any, type: string) { - return comp._listenEvents !== undefined && comp._listenEvents[type] !== undefined; - } - }; + public _hasListener(comp: any, type: string) { + return comp._listenEvents !== undefined && comp._listenEvents[type] !== undefined; + } } - -export class EventedComponent extends withEvent(Component) {} diff --git a/src/react-component/Anchor.css b/src/react-component/Anchor.css index fa78b1b..956368a 100644 --- a/src/react-component/Anchor.css +++ b/src/react-component/Anchor.css @@ -8,6 +8,7 @@ width: var(--graph-block-anchor-width, 16px); height: var(--graph-block-anchor-height, 16px); cursor: pointer; + will-change: transform; } .graph-block-anchor.graph-block-anchor-selected { diff --git a/src/services/camera/Camera.ts b/src/services/camera/Camera.ts index ecacbdc..8d4da05 100644 --- a/src/services/camera/Camera.ts +++ b/src/services/camera/Camera.ts @@ -1,6 +1,5 @@ import { TGraphLayerContext } from "../../components/canvas/layers/graphLayer/GraphLayer"; -import { Component } from "../../lib/Component"; -import { withEvent } from "../../mixins/withEvents"; +import { EventedComponent } from "../../mixins/withEvents"; import { getXY, isMetaKeyEvent, isTrackpadWheelEvent, isWindows } from "../../utils/functions"; import { clamp } from "../../utils/functions/clamp"; import { dragListener } from "../../utils/functions/dragListener"; @@ -12,7 +11,7 @@ export type TCameraProps = { root?: HTMLElement; }; -export class Camera extends withEvent(Component) { +export class Camera extends EventedComponent { public declare props: TCameraProps; public declare context: TGraphLayerContext; diff --git a/src/services/newConnection/ConnectionService.ts b/src/services/newConnection/ConnectionService.ts index c44c864..6e8c468 100644 --- a/src/services/newConnection/ConnectionService.ts +++ b/src/services/newConnection/ConnectionService.ts @@ -45,6 +45,15 @@ declare module "../../graphEvents" { targetAnchorId?: string; }> ) => void; + "connection-create-drop": ( + event: CustomEvent<{ + sourceBlockId: TBlockId; + sourceAnchorId: string; + targetBlockId?: TBlockId; + targetAnchorId?: string; + point: Point + }> + ) => void; } } @@ -169,15 +178,40 @@ export class ConnectionService extends Emitter { } } + protected getBlockId(component: BlockState | AnchorState) { + if (component instanceof AnchorState) { + return component.blockId; + } + return component.id; + } + + protected getAnchorId(component: BlockState | AnchorState) { + if (component instanceof AnchorState) { + return component.id; + } + return undefined; + } + public onEndNewConnection(point: Point) { + if (!this.sourceComponent) { + return; + } const targetComponent = this.graph.getElementOverPoint(point, [Block, Anchor]); this.emit(EVENTS.NEW_CONNECTION_END); if (!(targetComponent instanceof Block) && !(targetComponent instanceof Anchor)) { + this.graph.executеDefaultEventAction( + "connection-create-drop", + { + sourceBlockId: this.getBlockId(this.sourceComponent), + sourceAnchorId: this.getAnchorId(this.sourceComponent), + point, + }, + () => { } + ); return; } if ( - this.sourceComponent && targetComponent && targetComponent.connectedState && this.sourceComponent !== targetComponent.connectedState @@ -211,10 +245,21 @@ export class ConnectionService extends Emitter { } ); } - this.sourceComponent.setSelection(false); targetComponent.connectedState.setSelection(false); } + debugger; + this.graph.executеDefaultEventAction( + "connection-create-drop", + { + sourceBlockId: this.getBlockId(this.sourceComponent), + sourceAnchorId: this.getAnchorId(this.sourceComponent), + targetBlockId: this.getBlockId(targetComponent.connectedState), + targetAnchorId: this.getAnchorId(targetComponent.connectedState), + point, + }, + () => { } + ); } public unmount() { diff --git a/src/services/newConnection/NewConnection.ts b/src/services/newConnection/NewConnection.ts index 0c3cfa6..f8d668e 100644 --- a/src/services/newConnection/NewConnection.ts +++ b/src/services/newConnection/NewConnection.ts @@ -2,6 +2,7 @@ import { OverLayer, TOverLayerContext } from "../../components/canvas/layers/ove import { Component } from "../../lib/Component"; import { getXY } from "../../utils/functions"; import { render } from "../../utils/renderers/render"; +import { renderSVG } from "../../utils/renderers/svgPath"; import { EVENTS } from "../../utils/types/events"; import { ConnectionService } from "./ConnectionService"; @@ -32,16 +33,28 @@ export class NewConnection extends Component { } protected render() { - return render(this.context.ctx, (ctx) => { - ctx.strokeStyle = this.context.colors.connection.selectedBackground; - ctx.globalAlpha = 1; - + render(this.context.ctx, (ctx) => { ctx.beginPath(); + ctx.strokeStyle = this.context.colors.connection.selectedBackground; + ctx.setLineDash([4, 4]); ctx.moveTo(this.state.sx, this.state.sy); ctx.lineTo(this.state.tx, this.state.ty); ctx.stroke(); ctx.closePath(); }); + render(this.context.ctx, (ctx) => { + ctx.fillStyle = "rgba(254, 190, 92, 1)"; + ctx.roundRect(this.state.tx, this.state.ty - 12, 24, 24, 8); + ctx.fill(); + ctx.fillStyle = this.context.colors.canvas.belowLayerBackground; + renderSVG({ + path: `M7 0.75C7.41421 0.75 7.75 1.08579 7.75 1.5V6.25H12.5C12.9142 6.25 13.25 6.58579 13.25 7C13.25 7.41421 12.9142 7.75 12.5 7.75H7.75V12.5C7.75 12.9142 7.41421 13.25 7 13.25C6.58579 13.25 6.25 12.9142 6.25 12.5V7.75H1.5C1.08579 7.75 0.75 7.41421 0.75 7C0.75 6.58579 1.08579 6.25 1.5 6.25H6.25V1.5C6.25 1.08579 6.58579 0.75 7 0.75Z`, + width: 14, + height: 14, + iniatialWidth: 14, + initialHeight: 14 + }, ctx, { x: this.state.tx, y: this.state.ty - 12, width: 24, height: 24 }) + }) } private startNewConnectionRender = (event: MouseEvent) => { diff --git a/src/services/selection/SelectionArea.ts b/src/services/selection/SelectionArea.ts index 74c4d98..98be73f 100644 --- a/src/services/selection/SelectionArea.ts +++ b/src/services/selection/SelectionArea.ts @@ -3,6 +3,7 @@ import { Component } from "../../lib/Component"; import { getXY } from "../../utils/functions"; import { render } from "../../utils/renderers/render"; import { EVENTS } from "../../utils/types/events"; +import { Point } from "../../utils/types/shapes"; import { SelectionAreaService } from "./SelectionAreaService"; export type SelectionAreaProps = { @@ -16,14 +17,14 @@ export class SelectionArea extends Component { public declare props: SelectionAreaProps; + protected startPoint = new Point(0,0); + public constructor(props: SelectionAreaProps, context: OverLayer) { super(props, context); this.context = context.context; this.state = { - sx: 0, - sy: 0, width: 0, height: 0, }; @@ -34,13 +35,14 @@ export class SelectionArea extends Component { } protected render() { + if (!this.state.width && !this.state.height) { + return; + } return render(this.context.ctx, (ctx) => { ctx.fillStyle = this.context.colors.selection.background; ctx.strokeStyle = this.context.colors.selection.border; - ctx.globalAlpha = 1; - ctx.beginPath(); - ctx.rect(this.state.sx, this.state.sy, this.state.width, this.state.height); + ctx.roundRect(this.startPoint.x, this.startPoint.y, this.state.width, this.state.height, 1 * window.devicePixelRatio); ctx.closePath(); ctx.fill(); @@ -60,17 +62,15 @@ export class SelectionArea extends Component { private updateSelectionRender = (event: MouseEvent) => { const xy = getXY(this.context.graphCanvas, event); this.setState({ - width: xy[0] - this.state.sx, - height: xy[1] - this.state.sy, + width: xy[0] - this.startPoint.x, + height: xy[1] - this.startPoint.y, }); }; private startSelectionRender = (event: MouseEvent) => { - const xy = getXY(this.context.graphCanvas, event); - this.setState({ - sx: xy[0], - sy: xy[1], - }); + const [x,y] = getXY(this.context.graphCanvas, event); + this.startPoint.x = x; + this.startPoint.y = y; }; private endSelectionRender = () => { diff --git a/src/store/settings.ts b/src/store/settings.ts index 4ecae55..d3005e7 100644 --- a/src/store/settings.ts +++ b/src/store/settings.ts @@ -8,7 +8,7 @@ export enum ECanChangeBlockGeometry { NONE = "none", } -export type TGraphSettingsConfig = {}> = { +export type TGraphSettingsConfig = { canDragCamera: boolean; canZoomCamera: boolean; canDuplicateBlocks: boolean; @@ -21,7 +21,7 @@ export type TGraphSettingsConfig = {} useBlocksAnchors: boolean; connectivityComponentOnClickRaise: boolean; showConnectionLabels: boolean; - blockComponents: Record>>; + blockComponents: Record>; }; const getInitState: TGraphSettingsConfig = { diff --git a/src/stories/Playground/Editor/Editor.css b/src/stories/Playground/Editor/Editor.css new file mode 100644 index 0000000..b449338 --- /dev/null +++ b/src/stories/Playground/Editor/Editor.css @@ -0,0 +1,8 @@ +.editor-wrap { + width: 100%; +} + +.actions { + padding: var(--g-spacing-3); + border-top: 1px solid var(--g-color-base-float-accent-hover) +} \ No newline at end of file diff --git a/src/stories/Playground/Editor/index.tsx b/src/stories/Playground/Editor/index.tsx new file mode 100644 index 0000000..86bf72d --- /dev/null +++ b/src/stories/Playground/Editor/index.tsx @@ -0,0 +1,72 @@ + + +import { Editor, type Monaco, OnMount, loader } from "@monaco-editor/react"; +import React, { Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from "react"; +import type { TBlock } from "../../../components/canvas/blocks/Block"; +import type { TConnection } from "../../../store/connection/ConnectionState"; +import { defineTheme, GravityTheme } from "./theme"; +import { TBlockId } from "../../../store/block/Block"; +import { Button, Flex } from "@gravity-ui/uikit"; + +import "./Editor.css"; + + +loader.init().then((monaco) => { + defineTheme(monaco); +}); + +export interface ConfigEditorController { + scrollTo: (blockId: TBlockId) => void; +} + +export const ConfigEditor = React.forwardRef(function ConfigEditor({ blocks, connections }: { blocks: TBlock[], connections: TConnection[] }, ref: Ref) { + + const monacoRef = useRef[0]>(null); + + const blocksMap = useRef(new Map()); + + const value = useMemo(() => { + return JSON.stringify({ + blocks, + connections + }, null, 2); + }, [blocks, connections]); + + useImperativeHandle(ref, () => ({ + scrollTo: (blockId: string) => { + const symbolIndex = value.indexOf(`"id": "${blockId}"`); + if (symbolIndex >= 0) { + const line = Array.from(value.slice(0, symbolIndex).matchAll(/\n+/gm)).length + monacoRef.current?.revealLineNearTop(line, 0); + } + } + })) + + return + + { + monacoRef.current = editor; + }} + language={'json'} + value={value} + theme={GravityTheme} + options={{ + contextmenu: false, + lineNumbersMinChars: 2, + glyphMargin: false, + fontSize: 18, + lineHeight: 20, + colorDecorators: true, + minimap: {enabled: false}, + // @ts-ignore + 'bracketPairColorization.editor': true, + }} + /> + + + + + + +}) \ No newline at end of file diff --git a/src/stories/Playground/Editor/theme.ts b/src/stories/Playground/Editor/theme.ts new file mode 100644 index 0000000..4062655 --- /dev/null +++ b/src/stories/Playground/Editor/theme.ts @@ -0,0 +1,58 @@ +import type { Monaco } from "@monaco-editor/react"; + +export const GravityTheme = 'gravity'; + +export function defineTheme(monaco: Monaco) { + monaco.editor.defineTheme(GravityTheme, { + base: "vs-dark", + inherit: true, + rules: [ + { + token: "string.key.json", + foreground: "#febe5c", + }, + // { + // token: "string.value.json", + // foreground: "#ffffff", + // }, + // { + // token: "number.json", + // foreground: "#ffffff", + // }, + // { + // token: "keyword.json", + // foreground: "#ffffff", + // }, + // { + // token: "delimiter.bracket.json", + // foreground: "#ffffff", + // background: "#ffffff", + // }, + // { + // token: "delimiter.comma.json", + // foreground: "#ffffff", + // background: "#ffffff", + // }, + // { + // token: "delimiter.array.json", + // foreground: "#ffffff", + // background: "#ffffff", + // } + ], + colors: { + "editor.foreground": "#ffdb4d4d", + "editor.background": "#251b25", + "editor.lineHighlightBackground": "#ffdb4d4d", + "editorLineNumber.foreground": "#251b25", + "editor.selectionBackground": "#ffdb4d4d", + "editor.inactiveSelectionBackground": "#88000015", + // "editorBracketHighlight.foreground1": "#ffffff", + // "editorBracketHighlight.foreground2": "#ffffff", + // "editorBracketHighlight.foreground3": "#ffffff", + // "editorBracketHighlight.foreground4": "#ffffff", + // "editorBracketHighlight.foreground5": "#ffffff", + // "editorBracketHighlight.foreground6": "#ffffff", + // "editorBracketHighlight.unexpectedBracket.foreground": "#ffffff" + }, + }); +} \ No newline at end of file diff --git a/src/stories/Playground/GraphPlayground.tsx b/src/stories/Playground/GraphPlayground.tsx new file mode 100644 index 0000000..b1fd48a --- /dev/null +++ b/src/stories/Playground/GraphPlayground.tsx @@ -0,0 +1,133 @@ +import "@gravity-ui/uikit/styles/styles.css"; +import { useCallback, useLayoutEffect, useMemo, useRef } from "react"; +import { TBlock } from "../../components/canvas/blocks/Block"; +import { GraphState, Graph, TGraphConfig } from "../../graph"; +import { useGraph, useGraphEvent, GraphCanvas, GraphProps } from "../../react-component"; +import { useFn } from "../../utils/hooks/useFn"; +import { ECanChangeBlockGeometry } from "../../store/settings"; +import React from "react"; +import { PlaygroundBlock } from "./GravityBlock/GravityBlock"; +import { Flex, Text, ThemeContext, ThemeProvider } from "@gravity-ui/uikit"; +import { StoryFn } from "storybook/internal/types"; +import Editor from '@monaco-editor/react'; + +import './Playground.css'; +import { GravityBlock } from "./GravityBlock"; +import { IS_BLOCK_TYPE } from "../../store/block/Block"; +import { createPlaygroundBlock, generatePlaygroundLayout, GravityBlockIS, TGravityBlock } from "./generateLayout"; +import { ConfigEditor, ConfigEditorController } from "./Editor"; + +const generated = generatePlaygroundLayout(6, 12); + +export function GraphPLayground() { + const config = useMemo((): TGraphConfig => { + return { + blocks: [], + connections: [], + settings: { + canDragCamera: true, + canZoomCamera: true, + canDuplicateBlocks: false, + canChangeBlockGeometry: ECanChangeBlockGeometry.ALL, + canCreateNewConnections: false, + showConnectionArrows: false, + scaleFontSize: 1, + useBezierConnections: true, + useBlocksAnchors: true, + showConnectionLabels: false, + }, + }; + }, []); + const { graph, setEntities, start } = useGraph({ + viewConfiguration: { + colors: { + selection: { + background: "rgba(255, 190, 92, 0.1)", + border: "rgba(255, 190, 92, 1)", + }, + connection: { + background: "rgba(255, 255, 255, 0.5)", + selectedBackground: "rgba(234, 201, 74, 1)", + }, + block: { + background: "rgba(37, 27, 37, 1)", + border: "rgba(229, 229, 229, 0.2)", + selectedBorder: "rgba(255, 190, 92, 1)", + text: "rgba(255, 255, 255, 1)", + }, + anchor: { + background: "rgba(255, 190, 92, 1)" + }, + canvas: { + layerBackground: "rgba(22, 13, 27, 1)", + belowLayerBackground: "rgba(22, 13, 27, 1)", + dots: "rgba(255, 255, 255, 0.2)", + border: "rgba(255, 255, 255, 0.3)", + } + } + }, + settings: { + ...config.settings, + blockComponents: { + [GravityBlockIS]: GravityBlock + } + } + }); + + useGraphEvent(graph, 'connection-create-drop', ({targetBlockId, point}) => { + if(!targetBlockId) { + const block = createPlaygroundBlock(point.x, point.y, graph.rootStore.blocksList.$blocksMap.value.size + 1); + graph.api.addBlock(block); + graph.zoomTo([block.id], {transition: 250 }); + } + }); + + useLayoutEffect(() => { + setEntities({ blocks: generated.blocks, connections: generated.connections }); + }, [setEntities]); + + useGraphEvent(graph, "state-change", ({ state }) => { + if (state === GraphState.ATTACHED) { + start(); + graph.zoomTo("center", { padding: 300 }); + } + }); + + const renderBlockFn = useFn((graph: Graph, block: TGravityBlock) => { + const view = graph.rootStore.blocksList.getBlockState(block.id)?.getViewComponent() + if (view instanceof GravityBlock) { + return view.renderHTML(); + } + return + }); + + const ref = useRef(null); + + const onSelectBlock: GraphProps['onBlockSelectionChange'] = useCallback((selection) => { + if (selection.list.length === 1) { + ref?.current.scrollTo(selection.list[0]); + } + }, [graph]); + + + return ( + + + + Graph viewer + + + + + + JSON Editor + + + + + + + ); +} + +export const Default: StoryFn = () => ; \ No newline at end of file diff --git a/src/stories/Playground/GravityBlock/GravityBlock.css b/src/stories/Playground/GravityBlock/GravityBlock.css new file mode 100644 index 0000000..2f55a03 --- /dev/null +++ b/src/stories/Playground/GravityBlock/GravityBlock.css @@ -0,0 +1,20 @@ +.gravity-block-wrapper { + border-radius: 8px; + border-width: 3px; + padding: var(--g-spacing-3); + + display: flex; + flex-direction: column; + gap: var(--g-spacing-1); +} + +.gravity-block-name { + font-weight: 500; +} + +.gravity-block-wrapper:hover { + cursor: pointer; + background-color: rgba(57, 47, 57, 1); +} + + diff --git a/src/stories/Playground/GravityBlock/GravityBlock.tsx b/src/stories/Playground/GravityBlock/GravityBlock.tsx new file mode 100644 index 0000000..d016450 --- /dev/null +++ b/src/stories/Playground/GravityBlock/GravityBlock.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { Button, Flex, Icon, Text } from '@gravity-ui/uikit'; +import { Graph } from "../../../graph"; +import { GraphBlock, GraphBlockAnchor } from "../../../react-component"; +import { TGravityBlock } from "../generateLayout"; +import {Database} from '@gravity-ui/icons'; +import "./GravityBlock.css"; + +export function PlaygroundBlock({ graph, block }: { graph: Graph, block: TGravityBlock }) { + return ( + + {block.anchors.map((anchor) => { + return ( + + ); + })} + + {block.name} + {block.meta.description} + + + + + + ) +} \ No newline at end of file diff --git a/src/stories/Playground/GravityBlock/index.tsx b/src/stories/Playground/GravityBlock/index.tsx new file mode 100644 index 0000000..535ce9e --- /dev/null +++ b/src/stories/Playground/GravityBlock/index.tsx @@ -0,0 +1,140 @@ +import React from "react"; +import { CanvasBlock, EAnchorType, layoutText, TAnchor, TBlockId, TPoint } from "../../.."; +import { PlaygroundBlock } from "./GravityBlock"; + +import './GravityBlock.css'; +import { TBlockProps } from "../../../components/canvas/blocks/Block"; +import { render } from "../../../utils/renderers/render"; +import { TGravityBlock } from "../generateLayout"; +import { debugHitBox } from "../../../mixins/withHitTest"; +import { renderSVG } from "../../../utils/renderers/svgPath"; + +// font - family: YS Text; +// font - size: 9px; +// font - weight: 500; +// line - height: 12px; +// text - align: center; + + +export class GravityBlock extends CanvasBlock { + + public cursor = 'pointer'; + + protected hovered: boolean = false; + + protected subscribe(id: TBlockId) { + const subs = super.subscribe(id); + subs.push( + this.addEventListener('mouseenter', (e) => { + this.hovered = true; + this.performRender(); + } + ), + this.addEventListener('mouseleave', (e) => { + this.hovered = false; + this.performRender(); + }) + ) + return subs; + } + + protected renderName(ctx: CanvasRenderingContext2D) { + const scale = this.context.camera.getCameraScale(); + + if (scale > this.context.constants.block.SCALES[0]) { + ctx.fillStyle = this.context.colors.block.text; + ctx.textAlign = "center"; + this.renderText(this.state.name, ctx); + } + } + + public renderHTML() { + return + } + + protected renderStroke(color: string) { + const scale = this.context.camera.getCameraScale(); + this.context.ctx.lineWidth = Math.min(Math.round(3 / scale), 12); + this.context.ctx.strokeStyle = color; + this.context.ctx.strokeRect(this.state.x, this.state.y, this.state.width, this.state.height); + } + + public getAnchorPosition(anchor: TAnchor): TPoint { + return { + x: anchor.type === EAnchorType.OUT ? this.state.width : 0, + y: this.state.height / 2, + }; + } + + protected renderTextAtCenter(name: string, ctx: CanvasRenderingContext2D) { + const rect = this.getContentRect(); + const scale = this.context.camera.getCameraScale(); + ctx.fillStyle = this.context.colors.block.text; + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + const { lines, measures, lineHeight } = layoutText(name, ctx, rect, { font: `500 ${9 / scale}px YS Text`, lineHeight: 9 / scale }) + const shiftY = rect.height / 2 - measures.height / 2; + for (let index = 0; index < lines.length; index++) { + const [line, x, y] = lines[index]; + const rY = (y + shiftY) | 0; + ctx.fillText(line, x, rY); + } + } + + public renderBody(ctx: CanvasRenderingContext2D) { + const scale = this.context.camera.getCameraScale(); + + ctx.lineWidth = Math.min(Math.round(3 / scale), 12); + ctx.fillStyle = this.hovered ? "rgba(57, 47, 57, 1)" : this.context.colors.block.background; + + ctx.beginPath(); + ctx.roundRect(this.state.x, this.state.y, this.state.width, this.state.height, 8); + ctx.fill(); + if (this.state.selected) { + ctx.strokeStyle = this.context.colors.block.selectedBorder; + } else { + ctx.strokeStyle = this.hovered ? 'rgba(229, 229, 229, 0.4)' : this.context.colors.block.border; + } + ctx.stroke(); + ctx.closePath(); + } + + public renderMinimalisticBlock(ctx: CanvasRenderingContext2D): void { + render(ctx, (ctx) => { + this.renderBody(ctx); + ctx.beginPath(); + + // const path = new Path2D('M5.75 2.5H10.25C10.7842 2.5 11.2532 2.77929 11.519 3.19983C10.6259 3.58121 10 4.46751 10 5.5C10 6.61941 10.7357 7.56698 11.75 7.88555V12C11.75 12.8284 11.0784 13.5 10.25 13.5H5.75C5.21576 13.5 4.74676 13.2207 4.48102 12.8002C5.3741 12.4188 6 11.5325 6 10.5C6 9.38059 5.26428 8.43302 4.25 8.11445V7.88555C5.26428 7.56698 6 6.61941 6 5.5C6 4.46751 5.3741 3.58121 4.48102 3.19982C4.74676 2.77929 5.21576 2.5 5.75 2.5ZM2.75 8.11445V7.88555C1.73572 7.56698 1 6.61941 1 5.5C1 4.32762 1.80699 3.34373 2.8958 3.0735C3.28617 1.87008 4.41648 1 5.75 1H10.25C11.5835 1 12.7138 1.87008 13.1042 3.07351C14.193 3.34373 15 4.32762 15 5.5C15 6.61941 14.2643 7.56698 13.25 7.88555V12C13.25 13.6569 11.9069 15 10.25 15H5.75C4.41647 15 3.28616 14.1299 2.8958 12.9265C1.80699 12.6563 1 11.6724 1 10.5C1 9.38059 1.73572 8.43302 2.75 8.11445ZM3.5 11.5C4.05228 11.5 4.5 11.0523 4.5 10.5C4.5 9.94771 4.05228 9.5 3.5 9.5C2.94772 9.5 2.5 9.94772 2.5 10.5C2.5 11.0523 2.94772 11.5 3.5 11.5ZM2.5 5.5C2.5 4.94772 2.94772 4.5 3.5 4.5C4.05228 4.5 4.5 4.94772 4.5 5.5C4.5 6.05228 4.05228 6.5 3.5 6.5C2.94772 6.5 2.5 6.05229 2.5 5.5ZM12.5 4.5C11.9477 4.5 11.5 4.94772 11.5 5.5C11.5 6.05229 11.9477 6.5 12.5 6.5C13.0523 6.5 13.5 6.05229 13.5 5.5C13.5 4.94772 13.0523 4.5 12.5 4.5Z'); + ctx.fillStyle = "rgba(255, 190, 92, 1)"; + renderSVG({ + path: 'M5.75 2.5H10.25C10.7842 2.5 11.2532 2.77929 11.519 3.19983C10.6259 3.58121 10 4.46751 10 5.5C10 6.61941 10.7357 7.56698 11.75 7.88555V12C11.75 12.8284 11.0784 13.5 10.25 13.5H5.75C5.21576 13.5 4.74676 13.2207 4.48102 12.8002C5.3741 12.4188 6 11.5325 6 10.5C6 9.38059 5.26428 8.43302 4.25 8.11445V7.88555C5.26428 7.56698 6 6.61941 6 5.5C6 4.46751 5.3741 3.58121 4.48102 3.19982C4.74676 2.77929 5.21576 2.5 5.75 2.5ZM2.75 8.11445V7.88555C1.73572 7.56698 1 6.61941 1 5.5C1 4.32762 1.80699 3.34373 2.8958 3.0735C3.28617 1.87008 4.41648 1 5.75 1H10.25C11.5835 1 12.7138 1.87008 13.1042 3.07351C14.193 3.34373 15 4.32762 15 5.5C15 6.61941 14.2643 7.56698 13.25 7.88555V12C13.25 13.6569 11.9069 15 10.25 15H5.75C4.41647 15 3.28616 14.1299 2.8958 12.9265C1.80699 12.6563 1 11.6724 1 10.5C1 9.38059 1.73572 8.43302 2.75 8.11445ZM3.5 11.5C4.05228 11.5 4.5 11.0523 4.5 10.5C4.5 9.94771 4.05228 9.5 3.5 9.5C2.94772 9.5 2.5 9.94772 2.5 10.5C2.5 11.0523 2.94772 11.5 3.5 11.5ZM2.5 5.5C2.5 4.94772 2.94772 4.5 3.5 4.5C4.05228 4.5 4.5 4.94772 4.5 5.5C4.5 6.05228 4.05228 6.5 3.5 6.5C2.94772 6.5 2.5 6.05229 2.5 5.5ZM12.5 4.5C11.9477 4.5 11.5 4.94772 11.5 5.5C11.5 6.05229 11.9477 6.5 12.5 6.5C13.0523 6.5 13.5 6.05229 13.5 5.5C13.5 4.94772 13.0523 4.5 12.5 4.5Z', + width: 14 * 4, + height: 14 * 4, + iniatialWidth: 14, + initialHeight: 14, + }, ctx, this.getContentRect()); + // const scaleFactor = 4; + // const iconWidthWithScale = 16 * scaleFactor; + // const iconHeightWithScale = 16 * scaleFactor; + // ctx.translate(x + (width / 2) - iconWidthWithScale / 2, y + (height / 2) - iconHeightWithScale / 2); + // ctx.scale(scaleFactor, scaleFactor); + // ctx.fill(path, "evenodd"); + ctx.closePath(); + }) + } + + + public renderSchematicView(ctx: CanvasRenderingContext2D) { + this.renderBody(ctx); + + const scale = this.context.camera.getCameraScale(); + const shouldRenderText = scale > this.context.constants.block.SCALES[0]; + + if (shouldRenderText) { + ctx.fillStyle = this.context.colors.block.text; + ctx.textAlign = "center"; + this.renderTextAtCenter(this.state.name, ctx); + } + ctx.closePath(); + } +} \ No newline at end of file diff --git a/src/stories/Playground/Playground.css b/src/stories/Playground/Playground.css new file mode 100644 index 0000000..56d7a6a --- /dev/null +++ b/src/stories/Playground/Playground.css @@ -0,0 +1,25 @@ +.wrapper { + height: 100%; + background-color: rgba(37, 27, 37, 1); + padding: 16px; +} + +.graph { + min-width: 50% +} + +.graph-editor { + background-color: rgba(22, 13, 27, 1); +} + +.view { + border: 1px solid transparent; + border-radius: 24px; + overflow: hidden; + position: relative +} + +.view.config-editor { + border-radius: 10px; + border-color: var(--g-color-base-float-accent-hover); +} diff --git a/src/stories/Playground/Playground.stories.tsx b/src/stories/Playground/Playground.stories.tsx new file mode 100644 index 0000000..b548f60 --- /dev/null +++ b/src/stories/Playground/Playground.stories.tsx @@ -0,0 +1,13 @@ +import { Meta, StoryFn } from "@storybook/react/*"; +import { GraphPLayground } from "./GraphPlayground"; +import React from "react"; + +const meta: Meta = { + title: "Playground/playgground", + component: GraphPLayground, +}; + +export default meta; + +export const Default: StoryFn = () => ; + diff --git a/src/stories/Playground/generateLayout.tsx b/src/stories/Playground/generateLayout.tsx new file mode 100644 index 0000000..5899736 --- /dev/null +++ b/src/stories/Playground/generateLayout.tsx @@ -0,0 +1,104 @@ +import { IS_BLOCK_TYPE } from "../../store/block/Block"; +import { TBlock } from "../../components/canvas/blocks/Block"; +import { TGraphConfig } from "../../graph"; +import { EAnchorType } from "../../store/anchor/Anchor"; + +export const GravityBlockIS = 'gravity' +export type TGravityBlock = TBlock<{ description: string }> & { is: typeof GravityBlockIS }; + + +export function createPlaygroundBlock(x: number, y: number, index): TGravityBlock { + const blockId = `block_${index}`; + return { + id: blockId, + is: 'gravity', + x, + y, + width: 63 * window.devicePixelRatio, + height: 63 * window.devicePixelRatio, + selected: false, + name: `Block ${index}`, + meta: { + description: "Description", + }, + anchors: [ + { + id: `${blockId}_anchor_in`, + blockId: blockId, + type: EAnchorType.IN, + index: 0 + }, + { + id: `${blockId}_anchor_out`, + blockId: blockId, + type: EAnchorType.OUT, + index: 0 + } + ], + }; +} + +function getRandomArbitrary(min, max) { + return (Math.random() * (max - min) + min) | 0; +} + +export function generatePlaygroundLayout( + layersCount: number, + connectionsPerLayer: number, +) { + const config: TGraphConfig = { + blocks: [], + connections: [], + }; + + const gapX = 500; + const gapY = 200; + + const blocksMap = new Map(); + + let prevLayerBlocks: TBlock[] = []; + let index = 0; + for (let i = 0; i <= layersCount; i++) { + let count = i ** 2; + if (i >= layersCount / 2) { + count = (layersCount - i) ** 2; + } + const startY = (500 - gapY * count) / 2; + const layerX = gapX * i * 2.5; + const currentLayerBlocks: TBlock[] = []; + for (let j = 0; j <= count; j++) { + const y = startY + gapY * j; + + const block = createPlaygroundBlock(layerX, y, ++index); + + config.blocks.push(block); + currentLayerBlocks.push(block); + } + if (i > 1) { + for (let c = 0; c <= connectionsPerLayer; c++) { + const indexSource = getRandomArbitrary( + config.blocks.length - currentLayerBlocks.length - prevLayerBlocks.length - 1, + config.blocks.length - currentLayerBlocks.length - 1 + ); + const indexTarget = getRandomArbitrary( + config.blocks.length - currentLayerBlocks.length - 1, + config.blocks.length - 1 + ); + if (indexSource !== indexTarget) { + const sourceBlockId = `block_${indexSource}`; + const targetBlockId = `block_${indexTarget}`; + config.connections.push({ + sourceBlockId: sourceBlockId, + sourceAnchorId: `${sourceBlockId}_anchor_out`, + targetBlockId: targetBlockId, + targetAnchorId: `${targetBlockId}_anchor_in`, + }); + } + + } + prevLayerBlocks = [...currentLayerBlocks]; + } + } + + return config; +} diff --git a/src/stories/configurations/generatePretty.ts b/src/stories/configurations/generatePretty.ts index 601f05c..1fb206e 100644 --- a/src/stories/configurations/generatePretty.ts +++ b/src/stories/configurations/generatePretty.ts @@ -11,8 +11,8 @@ function createBlock(x: number, y: number, index): TBlock { is: IS_BLOCK_TYPE, x, y, - width: 200, - height: 150, + width: 63 * window.devicePixelRatio, + height: 63 * window.devicePixelRatio, selected: false, name: blockId, anchors: [], diff --git a/src/utils/renderers/svgPath.ts b/src/utils/renderers/svgPath.ts new file mode 100644 index 0000000..a734cb9 --- /dev/null +++ b/src/utils/renderers/svgPath.ts @@ -0,0 +1,20 @@ +export function renderSVG( + icon: { + path: string, + width: number, + height: number, + iniatialWidth: number, + initialHeight: number + }, + ctx: CanvasRenderingContext2D, + rect: {x: number, y: number, width: number, height: number}, + +) { + const iconPath = new Path2D(icon.path); + const coefX = icon.width / icon.iniatialWidth; + const coefY = icon.height / icon.initialHeight; + // MoveTo position + ctx.translate(rect.x + (rect.width / 2) - (icon.width / 2), rect.y + (rect.height / 2) - (icon.height / 2)); + ctx.scale(coefX, coefY); + ctx.fill(iconPath, 'evenodd'); +} diff --git a/src/utils/renderers/text.ts b/src/utils/renderers/text.ts index a79db9d..2c4a3d1 100644 --- a/src/utils/renderers/text.ts +++ b/src/utils/renderers/text.ts @@ -18,8 +18,8 @@ export function cachedMeasureText(text: string, params: TMeasureTextOptions) { } return cache.get(key); } -export type TTExtRect = Omit & Partial>; -export function renderText(text: string, ctx: CanvasRenderingContext2D, rect: TTExtRect, params: TMeasureTextOptions) { + +export function layoutText(text: string, ctx: CanvasRenderingContext2D, rect: TTExtRect, params: TMeasureTextOptions) { let x = rect.x; switch (ctx.textAlign) { @@ -47,14 +47,28 @@ export function renderText(text: string, ctx: CanvasRenderingContext2D, rect: TT maxHeight: rect.height, ...params, }); - + const lines = []; for (const line of measures.linesWords) { - ctx.fillText(line, x, y); + lines.push([line, x, y]); y += lineHeight; if (rect.height && y > rect.y + rect.height - lineHeight) { break; } } + return { + measures, + lines, + lineHeight, + } +} + +export type TTExtRect = Omit & Partial>; +export function renderText(text: string, ctx: CanvasRenderingContext2D, rect: TTExtRect, params: TMeasureTextOptions) { + const { lines, measures } = layoutText(text, ctx, rect, params); + + for (const [line, x, y] of lines) { + ctx.fillText(line, x, y); + } return measures; } From 5438d6904b5885b96b4b678ed0d5cb1e0ae78781 Mon Sep 17 00:00:00 2001 From: draedful Date: Tue, 8 Oct 2024 23:09:18 +0300 Subject: [PATCH 02/20] fix(Block): fix block's selection --- src/react-component/Block.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/react-component/Block.tsx b/src/react-component/Block.tsx index 06329b1..75df954 100644 --- a/src/react-component/Block.tsx +++ b/src/react-component/Block.tsx @@ -3,7 +3,7 @@ import { TBlock } from "../components/canvas/blocks/Block"; import { Graph } from "../graph"; import "./Block.css"; import { setCssProps } from "../utils/functions/cssProp"; -import { useBlockViewState } from "./hooks/useBlockState"; +import { useBlockState, useBlockViewState } from "./hooks/useBlockState"; export const GraphBlock = ({ graph, @@ -20,6 +20,7 @@ export const GraphBlock = ({ }) => { const containerRef = useRef(null); const viewState = useBlockViewState(graph, block); + const state = useBlockState(graph, block); useLayoutEffect(() => { setCssProps(containerRef.current, { @@ -44,7 +45,7 @@ export const GraphBlock = ({ return (
-
{children}
+
{children}
); }; From 6c08ed3465062539a1b1747fe0735ef128a749c1 Mon Sep 17 00:00:00 2001 From: draedful Date: Tue, 8 Oct 2024 23:20:30 +0300 Subject: [PATCH 03/20] playground: add connection after drop new connection --- src/stories/Playground/GraphPlayground.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/stories/Playground/GraphPlayground.tsx b/src/stories/Playground/GraphPlayground.tsx index b1fd48a..1e2d3e9 100644 --- a/src/stories/Playground/GraphPlayground.tsx +++ b/src/stories/Playground/GraphPlayground.tsx @@ -74,10 +74,16 @@ export function GraphPLayground() { } }); - useGraphEvent(graph, 'connection-create-drop', ({targetBlockId, point}) => { + useGraphEvent(graph, 'connection-create-drop', ({sourceBlockId, sourceAnchorId, targetBlockId, point}) => { if(!targetBlockId) { const block = createPlaygroundBlock(point.x, point.y, graph.rootStore.blocksList.$blocksMap.value.size + 1); graph.api.addBlock(block); + graph.api.addConnection({ + sourceBlockId, + sourceAnchorId, + targetBlockId: block.id, + targetAnchorId: block.anchors[0].id, + }) graph.zoomTo([block.id], {transition: 250 }); } }); From 7a77cad5bb4498f0370111cf1606b8ab2eeddbfe Mon Sep 17 00:00:00 2001 From: draedful Date: Tue, 8 Oct 2024 23:26:44 +0300 Subject: [PATCH 04/20] fix(Block): fix growing zIndex on select block --- src/react-component/Block.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/react-component/Block.css b/src/react-component/Block.css index 1cf4684..263c7a3 100644 --- a/src/react-component/Block.css +++ b/src/react-component/Block.css @@ -8,6 +8,8 @@ will-change: transform, width, height; width: var(--graph-block-geometry-width, 0); height: var(--graph-block-geometry-height, 0); + --z-index: calc(var(--graph-block-z-index, 0) + var(--graph-block-order, 0)); + z-index: var(--z-index); } .graph-block-wrapper { @@ -15,8 +17,6 @@ cursor: pointer; box-sizing: border-box; - --z-index: calc(var(--graph-block-z-index, 0) + var(--graph-block-order, 0)); - z-index: var(--z-index); background: var(--graph-block-bg); border: 1px solid var(--graph-block-border) From 3b6a4c77cf6850712f38cc8cfbc2df11cd8ee5ee Mon Sep 17 00:00:00 2001 From: draedful Date: Tue, 8 Oct 2024 23:27:54 +0300 Subject: [PATCH 05/20] fix ts in SelectionArea --- src/services/selection/SelectionArea.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/selection/SelectionArea.ts b/src/services/selection/SelectionArea.ts index 98be73f..5e4dc14 100644 --- a/src/services/selection/SelectionArea.ts +++ b/src/services/selection/SelectionArea.ts @@ -11,7 +11,7 @@ export type SelectionAreaProps = { }; export class SelectionArea extends Component { - public declare state: { sx: number; sy: number; width: number; height: number }; + public declare state: { width: number; height: number }; public declare context: TOverLayerContext; From c0ed5f60700bd42bf853773a6715867daf408db9 Mon Sep 17 00:00:00 2001 From: draedful Date: Wed, 9 Oct 2024 22:19:30 +0300 Subject: [PATCH 06/20] playground: update block-config in editor. --- src/react-component/Block.css | 1 + src/react-component/hooks/useSignal.ts | 6 +- .../newConnection/ConnectionService.ts | 2 +- src/stories/Playground/Editor/index.tsx | 76 +++++++++++---- src/stories/Playground/Editor/utils.ts | 29 ++++++ src/stories/Playground/GraphPlayground.tsx | 97 ++++++++++++------- src/stories/Playground/GravityBlock/index.tsx | 21 +--- 7 files changed, 155 insertions(+), 77 deletions(-) create mode 100644 src/stories/Playground/Editor/utils.ts diff --git a/src/react-component/Block.css b/src/react-component/Block.css index 263c7a3..c364861 100644 --- a/src/react-component/Block.css +++ b/src/react-component/Block.css @@ -14,6 +14,7 @@ .graph-block-wrapper { flex: 1; + min-width: 0; cursor: pointer; box-sizing: border-box; diff --git a/src/react-component/hooks/useSignal.ts b/src/react-component/hooks/useSignal.ts index 921cabb..37c1e95 100644 --- a/src/react-component/hooks/useSignal.ts +++ b/src/react-component/hooks/useSignal.ts @@ -4,10 +4,8 @@ import type { Signal } from "@preact/signals-core"; export function useSignal(signal: Signal) { const [state, setState] = useState(signal.value); - const ref = useRef(setState); - ref.current = setState; useEffect(() => { - return signal.subscribe((v) => ref.current(v)); - }, [ref, signal]); + return signal.subscribe(setState); + }, [signal]); return state; } diff --git a/src/services/newConnection/ConnectionService.ts b/src/services/newConnection/ConnectionService.ts index 6e8c468..e04c438 100644 --- a/src/services/newConnection/ConnectionService.ts +++ b/src/services/newConnection/ConnectionService.ts @@ -248,7 +248,7 @@ export class ConnectionService extends Emitter { this.sourceComponent.setSelection(false); targetComponent.connectedState.setSelection(false); } - debugger; + this.graph.executеDefaultEventAction( "connection-create-drop", { diff --git a/src/stories/Playground/Editor/index.tsx b/src/stories/Playground/Editor/index.tsx index 86bf72d..303fc4a 100644 --- a/src/stories/Playground/Editor/index.tsx +++ b/src/stories/Playground/Editor/index.tsx @@ -9,6 +9,7 @@ import { TBlockId } from "../../../store/block/Block"; import { Button, Flex } from "@gravity-ui/uikit"; import "./Editor.css"; +import { findBlockPositionsMonaco } from "./utils"; loader.init().then((monaco) => { @@ -17,39 +18,72 @@ loader.init().then((monaco) => { export interface ConfigEditorController { scrollTo: (blockId: TBlockId) => void; + updateBlocks: (block: TBlock[]) => void; + setContent: (p: {blocks: TBlock[], connections: TConnection[]}) => void; } -export const ConfigEditor = React.forwardRef(function ConfigEditor({ blocks, connections }: { blocks: TBlock[], connections: TConnection[] }, ref: Ref) { +type ConfigEditorProps = { + onChange?: (config: { blocks: TBlock[], connections: TConnection[] }) => void +}; - const monacoRef = useRef[0]>(null); +export const ConfigEditor = React.forwardRef(function ConfigEditor(props: ConfigEditorProps, ref: Ref) { - const blocksMap = useRef(new Map()); + const monacoRef = useRef[0]>(null); - const value = useMemo(() => { - return JSON.stringify({ - blocks, - connections - }, null, 2); - }, [blocks, connections]); + const valueRef = useRef<{ blocks: TBlock[], connections: TConnection[] }>({ blocks: [], connections: []}) useImperativeHandle(ref, () => ({ scrollTo: (blockId: string) => { - const symbolIndex = value.indexOf(`"id": "${blockId}"`); - if (symbolIndex >= 0) { - const line = Array.from(value.slice(0, symbolIndex).matchAll(/\n+/gm)).length - monacoRef.current?.revealLineNearTop(line, 0); + + const model = monacoRef.current.getModel(); + const range = findBlockPositionsMonaco(model, blockId); + + if (range?.start.column) { + monacoRef.current?.revealLinesInCenter(range.start.lineNumber, range.end.lineNumber, 0); + } + + monacoRef.current.setSelection({ + startColumn: range.start.column, + startLineNumber: range.start.lineNumber, + endColumn: range.end.column, + endLineNumber: range.end.lineNumber + }); + }, + updateBlocks: (blocks: TBlock[]) => { + const model = monacoRef.current.getModel(); + const edits = blocks.map((block)=> { + const range = findBlockPositionsMonaco(model, block.id); + const text = JSON.stringify({ block: [block] }, null, 2); + return { + range: { + startColumn: range.start.column, + startLineNumber: range.start.lineNumber, + endColumn: range.end.column, + endLineNumber: range.end.lineNumber, + }, + text: text.slice(19, text.length - 6), + } + }) + + model.applyEdits(edits); + }, + setContent: ({ blocks, connections}) => { + valueRef.current = { + blocks, + connections } - } - })) + monacoRef.current?.setValue(JSON.stringify(valueRef.current, null, 2)); + }, + })); return { monacoRef.current = editor; + monacoRef.current?.setValue(JSON.stringify(valueRef.current, null, 2)); }} language={'json'} - value={value} theme={GravityTheme} options={{ contextmenu: false, @@ -59,13 +93,21 @@ export const ConfigEditor = React.forwardRef(function ConfigEditor({ blocks, con lineHeight: 20, colorDecorators: true, minimap: {enabled: false}, + smoothScrolling: true, // @ts-ignore 'bracketPairColorization.editor': true, }} /> - + diff --git a/src/stories/Playground/Editor/utils.ts b/src/stories/Playground/Editor/utils.ts new file mode 100644 index 0000000..30fd663 --- /dev/null +++ b/src/stories/Playground/Editor/utils.ts @@ -0,0 +1,29 @@ +export function findBlockPositionsMonaco(model, blockId) { + const configString = model.getValue(); + const blockSearchStr = `"id": "${blockId}"`; + const startIndex = configString.indexOf(blockSearchStr); + + const blockStart = configString.lastIndexOf('{', startIndex); + let blockEnd = configString.indexOf('}', startIndex); + + let braceCount = 1; + let currentPos = blockEnd + 1; + while (braceCount > 0 && currentPos < configString.length) { + if (configString[currentPos] === '{') { + braceCount++; + } else if (configString[currentPos] === '}') { + braceCount--; + } + currentPos++; + } + blockEnd = currentPos; + + // Получаем позиции начала и конца через Monaco Editor API + const startPosition = model.getPositionAt(blockStart); + const endPosition = model.getPositionAt(blockEnd); + + return { + start: startPosition, + end: endPosition, + }; +} diff --git a/src/stories/Playground/GraphPlayground.tsx b/src/stories/Playground/GraphPlayground.tsx index 1e2d3e9..81152c3 100644 --- a/src/stories/Playground/GraphPlayground.tsx +++ b/src/stories/Playground/GraphPlayground.tsx @@ -1,44 +1,22 @@ +import { Flex, Text, ThemeProvider } from "@gravity-ui/uikit"; import "@gravity-ui/uikit/styles/styles.css"; -import { useCallback, useLayoutEffect, useMemo, useRef } from "react"; -import { TBlock } from "../../components/canvas/blocks/Block"; -import { GraphState, Graph, TGraphConfig } from "../../graph"; -import { useGraph, useGraphEvent, GraphCanvas, GraphProps } from "../../react-component"; -import { useFn } from "../../utils/hooks/useFn"; +import React, { useCallback, useLayoutEffect, useRef, useState } from "react"; +import { StoryFn } from "storybook/internal/types"; +import { Graph, GraphState, TGraphConfig } from "../../graph"; +import { GraphCanvas, GraphProps, useGraph, useGraphEvent } from "../../react-component"; import { ECanChangeBlockGeometry } from "../../store/settings"; -import React from "react"; +import { useFn } from "../../utils/hooks/useFn"; import { PlaygroundBlock } from "./GravityBlock/GravityBlock"; -import { Flex, Text, ThemeContext, ThemeProvider } from "@gravity-ui/uikit"; -import { StoryFn } from "storybook/internal/types"; -import Editor from '@monaco-editor/react'; -import './Playground.css'; -import { GravityBlock } from "./GravityBlock"; -import { IS_BLOCK_TYPE } from "../../store/block/Block"; -import { createPlaygroundBlock, generatePlaygroundLayout, GravityBlockIS, TGravityBlock } from "./generateLayout"; import { ConfigEditor, ConfigEditorController } from "./Editor"; +import { createPlaygroundBlock, generatePlaygroundLayout, GravityBlockIS, TGravityBlock } from "./generateLayout"; +import { GravityBlock } from "./GravityBlock"; +import './Playground.css'; -const generated = generatePlaygroundLayout(6, 12); +const generated = generatePlaygroundLayout(0, 5); export function GraphPLayground() { - const config = useMemo((): TGraphConfig => { - return { - blocks: [], - connections: [], - settings: { - canDragCamera: true, - canZoomCamera: true, - canDuplicateBlocks: false, - canChangeBlockGeometry: ECanChangeBlockGeometry.ALL, - canCreateNewConnections: false, - showConnectionArrows: false, - scaleFontSize: 1, - useBezierConnections: true, - useBlocksAnchors: true, - showConnectionLabels: false, - }, - }; - }, []); - const { graph, setEntities, start } = useGraph({ + const { graph, setEntities, updateEntities, start } = useGraph({ viewConfiguration: { colors: { selection: { @@ -67,13 +45,51 @@ export function GraphPLayground() { } }, settings: { - ...config.settings, + canDragCamera: true, + canZoomCamera: true, + canDuplicateBlocks: false, + canChangeBlockGeometry: ECanChangeBlockGeometry.ALL, + canCreateNewConnections: false, + showConnectionArrows: false, + scaleFontSize: 1, + useBezierConnections: true, + useBlocksAnchors: true, + showConnectionLabels: false, blockComponents: { [GravityBlockIS]: GravityBlock } } }); + const updateVisibleConfig = useFn(() => { + const config = graph.rootStore.getAsConfig(); + editorRef?.current.setContent({ + blocks: config.blocks || [], + connections: config.connections || [] + }); + }) + + useGraphEvent(graph, 'block-drag', ({block}) => { + editorRef?.current.scrollTo(block.id); + }) + + useGraphEvent(graph, 'block-change', ({block}) => { + editorRef?.current.updateBlocks([block]); + editorRef?.current.scrollTo(block.id); + }); + useGraphEvent(graph, "blocks-selection-change", ({ changes }) => { + editorRef?.current.updateBlocks([ + ...changes.add.map((id) => ({ + ...graph.rootStore.blocksList.getBlock(id), + selected: true, + })), + ...changes.removed.map((id) => ({ + ...graph.rootStore.blocksList.getBlock(id), + selected: false, + })) + ]); + }); + useGraphEvent(graph, 'connection-create-drop', ({sourceBlockId, sourceAnchorId, targetBlockId, point}) => { if(!targetBlockId) { const block = createPlaygroundBlock(point.x, point.y, graph.rootStore.blocksList.$blocksMap.value.size + 1); @@ -85,11 +101,14 @@ export function GraphPLayground() { targetAnchorId: block.anchors[0].id, }) graph.zoomTo([block.id], {transition: 250 }); + updateVisibleConfig(); + editorRef?.current.scrollTo(block.id); } }); useLayoutEffect(() => { setEntities({ blocks: generated.blocks, connections: generated.connections }); + updateVisibleConfig(); }, [setEntities]); useGraphEvent(graph, "state-change", ({ state }) => { @@ -107,15 +126,16 @@ export function GraphPLayground() { return }); - const ref = useRef(null); + const editorRef = useRef(null); const onSelectBlock: GraphProps['onBlockSelectionChange'] = useCallback((selection) => { if (selection.list.length === 1) { - ref?.current.scrollTo(selection.list[0]); + editorRef?.current.scrollTo(selection.list[0]); } }, [graph]); + return ( @@ -128,7 +148,10 @@ export function GraphPLayground() { JSON Editor - + { + debugger; + updateEntities({blocks, connections}) + }} ref={editorRef} /> diff --git a/src/stories/Playground/GravityBlock/index.tsx b/src/stories/Playground/GravityBlock/index.tsx index 535ce9e..01cf632 100644 --- a/src/stories/Playground/GravityBlock/index.tsx +++ b/src/stories/Playground/GravityBlock/index.tsx @@ -2,18 +2,10 @@ import React from "react"; import { CanvasBlock, EAnchorType, layoutText, TAnchor, TBlockId, TPoint } from "../../.."; import { PlaygroundBlock } from "./GravityBlock"; -import './GravityBlock.css'; -import { TBlockProps } from "../../../components/canvas/blocks/Block"; import { render } from "../../../utils/renderers/render"; -import { TGravityBlock } from "../generateLayout"; -import { debugHitBox } from "../../../mixins/withHitTest"; import { renderSVG } from "../../../utils/renderers/svgPath"; - -// font - family: YS Text; -// font - size: 9px; -// font - weight: 500; -// line - height: 12px; -// text - align: center; +import { TGravityBlock } from "../generateLayout"; +import './GravityBlock.css'; export class GravityBlock extends CanvasBlock { @@ -49,7 +41,7 @@ export class GravityBlock extends CanvasBlock { } public renderHTML() { - return + return } protected renderStroke(color: string) { @@ -104,7 +96,6 @@ export class GravityBlock extends CanvasBlock { this.renderBody(ctx); ctx.beginPath(); - // const path = new Path2D('M5.75 2.5H10.25C10.7842 2.5 11.2532 2.77929 11.519 3.19983C10.6259 3.58121 10 4.46751 10 5.5C10 6.61941 10.7357 7.56698 11.75 7.88555V12C11.75 12.8284 11.0784 13.5 10.25 13.5H5.75C5.21576 13.5 4.74676 13.2207 4.48102 12.8002C5.3741 12.4188 6 11.5325 6 10.5C6 9.38059 5.26428 8.43302 4.25 8.11445V7.88555C5.26428 7.56698 6 6.61941 6 5.5C6 4.46751 5.3741 3.58121 4.48102 3.19982C4.74676 2.77929 5.21576 2.5 5.75 2.5ZM2.75 8.11445V7.88555C1.73572 7.56698 1 6.61941 1 5.5C1 4.32762 1.80699 3.34373 2.8958 3.0735C3.28617 1.87008 4.41648 1 5.75 1H10.25C11.5835 1 12.7138 1.87008 13.1042 3.07351C14.193 3.34373 15 4.32762 15 5.5C15 6.61941 14.2643 7.56698 13.25 7.88555V12C13.25 13.6569 11.9069 15 10.25 15H5.75C4.41647 15 3.28616 14.1299 2.8958 12.9265C1.80699 12.6563 1 11.6724 1 10.5C1 9.38059 1.73572 8.43302 2.75 8.11445ZM3.5 11.5C4.05228 11.5 4.5 11.0523 4.5 10.5C4.5 9.94771 4.05228 9.5 3.5 9.5C2.94772 9.5 2.5 9.94772 2.5 10.5C2.5 11.0523 2.94772 11.5 3.5 11.5ZM2.5 5.5C2.5 4.94772 2.94772 4.5 3.5 4.5C4.05228 4.5 4.5 4.94772 4.5 5.5C4.5 6.05228 4.05228 6.5 3.5 6.5C2.94772 6.5 2.5 6.05229 2.5 5.5ZM12.5 4.5C11.9477 4.5 11.5 4.94772 11.5 5.5C11.5 6.05229 11.9477 6.5 12.5 6.5C13.0523 6.5 13.5 6.05229 13.5 5.5C13.5 4.94772 13.0523 4.5 12.5 4.5Z'); ctx.fillStyle = "rgba(255, 190, 92, 1)"; renderSVG({ path: 'M5.75 2.5H10.25C10.7842 2.5 11.2532 2.77929 11.519 3.19983C10.6259 3.58121 10 4.46751 10 5.5C10 6.61941 10.7357 7.56698 11.75 7.88555V12C11.75 12.8284 11.0784 13.5 10.25 13.5H5.75C5.21576 13.5 4.74676 13.2207 4.48102 12.8002C5.3741 12.4188 6 11.5325 6 10.5C6 9.38059 5.26428 8.43302 4.25 8.11445V7.88555C5.26428 7.56698 6 6.61941 6 5.5C6 4.46751 5.3741 3.58121 4.48102 3.19982C4.74676 2.77929 5.21576 2.5 5.75 2.5ZM2.75 8.11445V7.88555C1.73572 7.56698 1 6.61941 1 5.5C1 4.32762 1.80699 3.34373 2.8958 3.0735C3.28617 1.87008 4.41648 1 5.75 1H10.25C11.5835 1 12.7138 1.87008 13.1042 3.07351C14.193 3.34373 15 4.32762 15 5.5C15 6.61941 14.2643 7.56698 13.25 7.88555V12C13.25 13.6569 11.9069 15 10.25 15H5.75C4.41647 15 3.28616 14.1299 2.8958 12.9265C1.80699 12.6563 1 11.6724 1 10.5C1 9.38059 1.73572 8.43302 2.75 8.11445ZM3.5 11.5C4.05228 11.5 4.5 11.0523 4.5 10.5C4.5 9.94771 4.05228 9.5 3.5 9.5C2.94772 9.5 2.5 9.94772 2.5 10.5C2.5 11.0523 2.94772 11.5 3.5 11.5ZM2.5 5.5C2.5 4.94772 2.94772 4.5 3.5 4.5C4.05228 4.5 4.5 4.94772 4.5 5.5C4.5 6.05228 4.05228 6.5 3.5 6.5C2.94772 6.5 2.5 6.05229 2.5 5.5ZM12.5 4.5C11.9477 4.5 11.5 4.94772 11.5 5.5C11.5 6.05229 11.9477 6.5 12.5 6.5C13.0523 6.5 13.5 6.05229 13.5 5.5C13.5 4.94772 13.0523 4.5 12.5 4.5Z', @@ -113,12 +104,6 @@ export class GravityBlock extends CanvasBlock { iniatialWidth: 14, initialHeight: 14, }, ctx, this.getContentRect()); - // const scaleFactor = 4; - // const iconWidthWithScale = 16 * scaleFactor; - // const iconHeightWithScale = 16 * scaleFactor; - // ctx.translate(x + (width / 2) - iconWidthWithScale / 2, y + (height / 2) - iconHeightWithScale / 2); - // ctx.scale(scaleFactor, scaleFactor); - // ctx.fill(path, "evenodd"); ctx.closePath(); }) } From b6f975d2f6aba2c39b22a1dc5795619465deab8f Mon Sep 17 00:00:00 2001 From: draedful Date: Wed, 9 Oct 2024 22:52:36 +0300 Subject: [PATCH 07/20] playground: allow add new blocks --- src/stories/Playground/Editor/index.tsx | 3 ++- src/stories/Playground/GraphPlayground.tsx | 31 ++++++++++++++++------ src/stories/Playground/generateLayout.tsx | 2 -- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/stories/Playground/Editor/index.tsx b/src/stories/Playground/Editor/index.tsx index 303fc4a..0391fb5 100644 --- a/src/stories/Playground/Editor/index.tsx +++ b/src/stories/Playground/Editor/index.tsx @@ -24,6 +24,7 @@ export interface ConfigEditorController { type ConfigEditorProps = { onChange?: (config: { blocks: TBlock[], connections: TConnection[] }) => void + addBlock?: () => void }; export const ConfigEditor = React.forwardRef(function ConfigEditor(props: ConfigEditorProps, ref: Ref) { @@ -108,7 +109,7 @@ export const ConfigEditor = React.forwardRef(function ConfigEditor(props: Config console.error(e); } }}>Apply - + }) \ No newline at end of file diff --git a/src/stories/Playground/GraphPlayground.tsx b/src/stories/Playground/GraphPlayground.tsx index 81152c3..2e244a1 100644 --- a/src/stories/Playground/GraphPlayground.tsx +++ b/src/stories/Playground/GraphPlayground.tsx @@ -2,8 +2,8 @@ import { Flex, Text, ThemeProvider } from "@gravity-ui/uikit"; import "@gravity-ui/uikit/styles/styles.css"; import React, { useCallback, useLayoutEffect, useRef, useState } from "react"; import { StoryFn } from "storybook/internal/types"; -import { Graph, GraphState, TGraphConfig } from "../../graph"; -import { GraphCanvas, GraphProps, useGraph, useGraphEvent } from "../../react-component"; +import { Graph, GraphState } from "../../graph"; +import { GraphBlock, GraphCanvas, GraphProps, useGraph, useGraphEvent } from "../../react-component"; import { ECanChangeBlockGeometry } from "../../store/settings"; import { useFn } from "../../utils/hooks/useFn"; import { PlaygroundBlock } from "./GravityBlock/GravityBlock"; @@ -12,6 +12,8 @@ import { ConfigEditor, ConfigEditorController } from "./Editor"; import { createPlaygroundBlock, generatePlaygroundLayout, GravityBlockIS, TGravityBlock } from "./generateLayout"; import { GravityBlock } from "./GravityBlock"; import './Playground.css'; +import { TBlock } from "../../components/canvas/blocks/Block"; +import { random } from "../../components/canvas/blocks/generate"; const generated = generatePlaygroundLayout(0, 5); @@ -118,12 +120,23 @@ export function GraphPLayground() { } }); - const renderBlockFn = useFn((graph: Graph, block: TGravityBlock) => { + const addNewBlock = useFn(() => { + const rect = graph.rootStore.blocksList.getUsableRect(); + const x = random(rect.x, rect.x + rect.width * 2); + const y = random(rect.y, rect.y + rect.height * 2); + const block = createPlaygroundBlock(x, y, graph.rootStore.blocksList.$blocksMap.value.size + 1); + graph.api.addBlock(block); + graph.zoomTo([block.id], { transition: 250 }); + updateVisibleConfig(); + editorRef?.current.scrollTo(block.id); + }); + + const renderBlockFn = useFn((graph: Graph, block: TBlock) => { const view = graph.rootStore.blocksList.getBlockState(block.id)?.getViewComponent() if (view instanceof GravityBlock) { return view.renderHTML(); } - return + return Unknown block <>{block.id} }); const editorRef = useRef(null); @@ -148,10 +161,12 @@ export function GraphPLayground() { JSON Editor - { - debugger; - updateEntities({blocks, connections}) - }} ref={editorRef} /> + { + updateEntities({blocks, connections}) + }} + addBlock={addNewBlock} /> diff --git a/src/stories/Playground/generateLayout.tsx b/src/stories/Playground/generateLayout.tsx index 5899736..84b8677 100644 --- a/src/stories/Playground/generateLayout.tsx +++ b/src/stories/Playground/generateLayout.tsx @@ -54,8 +54,6 @@ export function generatePlaygroundLayout( const gapX = 500; const gapY = 200; - const blocksMap = new Map(); - let prevLayerBlocks: TBlock[] = []; let index = 0; for (let i = 0; i <= layersCount; i++) { From 6da0193f7d871551a9207d16349f8aba1b6a1ec3 Mon Sep 17 00:00:00 2001 From: draedful Date: Thu, 10 Oct 2024 00:52:34 +0300 Subject: [PATCH 08/20] playground: Add validation errors --- src/components/canvas/blocks/Block.ts | 1 - src/stories/Playground/Editor/index.tsx | 50 ++++++- src/stories/Playground/Editor/schema.ts | 156 +++++++++++++++++++++ src/stories/Playground/GraphPlayground.tsx | 4 +- 4 files changed, 201 insertions(+), 10 deletions(-) create mode 100644 src/stories/Playground/Editor/schema.ts diff --git a/src/components/canvas/blocks/Block.ts b/src/components/canvas/blocks/Block.ts index fb6fcbd..deb8777 100644 --- a/src/components/canvas/blocks/Block.ts +++ b/src/components/canvas/blocks/Block.ts @@ -249,7 +249,6 @@ export class Block { defineTheme(monaco); + defineConigSchema(monaco); }); export interface ConfigEditorController { @@ -27,15 +29,21 @@ type ConfigEditorProps = { addBlock?: () => void }; +type ExtractTypeFromArray = T extends Array ? E : never; + export const ConfigEditor = React.forwardRef(function ConfigEditor(props: ConfigEditorProps, ref: Ref) { + const [errorMarker, setErrorMarker] = useState[0]>>(null); + const monacoRef = useRef[0]>(null); const valueRef = useRef<{ blocks: TBlock[], connections: TConnection[] }>({ blocks: [], connections: []}) useImperativeHandle(ref, () => ({ scrollTo: (blockId: string) => { - + if (!monacoRef.current) { + return; + } const model = monacoRef.current.getModel(); const range = findBlockPositionsMonaco(model, blockId); @@ -51,6 +59,9 @@ export const ConfigEditor = React.forwardRef(function ConfigEditor(props: Config }); }, updateBlocks: (blocks: TBlock[]) => { + if (!monacoRef.current) { + return; + } const model = monacoRef.current.getModel(); const edits = blocks.map((block)=> { const range = findBlockPositionsMonaco(model, block.id); @@ -73,10 +84,14 @@ export const ConfigEditor = React.forwardRef(function ConfigEditor(props: Config blocks, connections } + if (!monacoRef.current) { + return; + } monacoRef.current?.setValue(JSON.stringify(valueRef.current, null, 2)); }, })); + return { + setErrorMarker(markers.filter((m) => m.severity === 8)[0] || null) + }} language={'json'} theme={GravityTheme} options={{ @@ -101,7 +119,7 @@ export const ConfigEditor = React.forwardRef(function ConfigEditor(props: Config /> - - + + {errorMarker && ( + + + { + monacoRef.current?.revealLinesInCenter(errorMarker.startLineNumber, errorMarker.endLineNumber, 0); + + + monacoRef.current.setSelection({ + startColumn: errorMarker.startColumn, + startLineNumber: errorMarker.startLineNumber, + endColumn: errorMarker.endColumn, + endLineNumber: errorMarker.endLineNumber + }); + }} + >{errorMarker.message} + + + )} }) \ No newline at end of file diff --git a/src/stories/Playground/Editor/schema.ts b/src/stories/Playground/Editor/schema.ts new file mode 100644 index 0000000..37f451f --- /dev/null +++ b/src/stories/Playground/Editor/schema.ts @@ -0,0 +1,156 @@ +import { Monaco } from "@monaco-editor/react"; + +export function defineConigSchema(monaco: Monaco) { + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: true, + schemaValidation: "error", + schemas: [ + { + uri: "http://gravity/graph-playground/schema.json", // id of the first schema + fileMatch: ["*"], // associate with our model + schema: { + "type": "object", + "properties": { + "blocks": { + "type": "array", + "items": { + "$ref": "#/definitions/TBlock" + }, + "description": "List of blocks (TBlock[])" + }, + "connections": { + "type": "array", + "items": { + "$ref": "#/definitions/TConnection" + }, + "description": "List of connections (TConnection[])" + } + }, + "required": ["blocks", "connections"], + "definitions": { + "TBlock": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Block identifier (TBlockId)" + }, + "is": { + "type": "string", + "description": "String representation of the block type" + }, + "x": { + "type": "number", + "description": "X coordinate" + }, + "y": { + "type": "number", + "description": "Y coordinate" + }, + "width": { + "type": "number", + "description": "Block width" + }, + "height": { + "type": "number", + "description": "Block height" + }, + "selected": { + "type": "boolean", + "description": "Flag indicating if the block is selected" + }, + "name": { + "type": "string", + "description": "Block name" + }, + "anchors": { + "type": "array", + "items": { + "$ref": "#/definitions/TAnchor" + }, + "description": "List of anchors (TAnchor[])" + }, + "meta": { + "type": "object", + "description": "Meta information (optional)", + } + }, + "required": ["id", "is", "x", "y", "width", "height", "selected", "name", "anchors"] + }, + "TConnection": { + "type": "object", + "properties": { + "sourceBlockId": { + "type": "string", + "description": "Identifier of the source block" + }, + "targetBlockId": { + "type": "string", + "description": "Identifier of the target block" + }, + "sourceAnchorId": { + "type": "string", + "description": "Identifier of the source anchor (optional)" + }, + "targetAnchorId": { + "type": "string", + "description": "Identifier of the target anchor (optional)" + }, + "label": { + "type": "string", + "description": "Connection label (optional)" + }, + "styles": { + "type": "object", + "properties": { + "dashes": { + "type": "array", + "items": { + "type": "number" + }, + "description": "Array of dash lengths for dashed lines (optional)" + } + }, + "additionalProperties": true, + "description": "Connection styles (optional)" + }, + "dashed": { + "type": "boolean", + "description": "Flag indicating if the line is dashed (optional)" + }, + "selected": { + "type": "boolean", + "description": "Flag indicating if the connection is selected (optional)" + } + }, + "required": ["sourceBlockId", "targetBlockId"] + }, + "TAnchor": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Anchor identifier" + }, + "blockId": { + "type": "string", + "description": "Identifier of the block this anchor belongs to" + }, + "type": { + "type": "string", + "enum": ["IN", "OUT"], + "description": "Anchor type, either IN or OUT" + }, + "index": { + "type": "number", + "description": "Anchor index" + } + }, + "required": ["id", "blockId", "type", "index"] + } + } + }, + }, + ], + }); +} \ No newline at end of file diff --git a/src/stories/Playground/GraphPlayground.tsx b/src/stories/Playground/GraphPlayground.tsx index 2e244a1..2c89de7 100644 --- a/src/stories/Playground/GraphPlayground.tsx +++ b/src/stories/Playground/GraphPlayground.tsx @@ -122,8 +122,8 @@ export function GraphPLayground() { const addNewBlock = useFn(() => { const rect = graph.rootStore.blocksList.getUsableRect(); - const x = random(rect.x, rect.x + rect.width * 2); - const y = random(rect.y, rect.y + rect.height * 2); + const x = random(rect.x, rect.x + rect.width + 100); + const y = random(rect.y, rect.y + rect.height + 100); const block = createPlaygroundBlock(x, y, graph.rootStore.blocksList.$blocksMap.value.size + 1); graph.api.addBlock(block); graph.zoomTo([block.id], { transition: 250 }); From 3a4678084b0ca7cad30bd2561ca189ffcaf512c0 Mon Sep 17 00:00:00 2001 From: draedful Date: Thu, 10 Oct 2024 02:20:38 +0300 Subject: [PATCH 09/20] playground: fix flickering block on switch levels --- src/stories/Playground/GravityBlock/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/stories/Playground/GravityBlock/index.tsx b/src/stories/Playground/GravityBlock/index.tsx index 01cf632..0fbc49b 100644 --- a/src/stories/Playground/GravityBlock/index.tsx +++ b/src/stories/Playground/GravityBlock/index.tsx @@ -108,6 +108,10 @@ export class GravityBlock extends CanvasBlock { }) } + public renderDetailedView(ctx: CanvasRenderingContext2D) { + // This needs to prevent flickering of block on switch levels + this.renderBody(ctx); + } public renderSchematicView(ctx: CanvasRenderingContext2D) { this.renderBody(ctx); From 585a24a7116c2b11eabb4380b40062bb1d8ab590 Mon Sep 17 00:00:00 2001 From: draedful Date: Thu, 10 Oct 2024 03:08:53 +0300 Subject: [PATCH 10/20] playground: add toolbox --- src/stories/Playground/GraphPlayground.tsx | 99 ++++++++++++---------- src/stories/Playground/Playground.css | 55 +++++++++++- src/stories/Playground/Settings.tsx | 60 +++++++++++++ src/stories/Playground/Toolbox.tsx | 39 +++++++++ src/stories/Playground/hooks.ts | 9 ++ 5 files changed, 215 insertions(+), 47 deletions(-) create mode 100644 src/stories/Playground/Settings.tsx create mode 100644 src/stories/Playground/Toolbox.tsx create mode 100644 src/stories/Playground/hooks.ts diff --git a/src/stories/Playground/GraphPlayground.tsx b/src/stories/Playground/GraphPlayground.tsx index 2c89de7..90fe97d 100644 --- a/src/stories/Playground/GraphPlayground.tsx +++ b/src/stories/Playground/GraphPlayground.tsx @@ -1,4 +1,4 @@ -import { Flex, Text, ThemeProvider } from "@gravity-ui/uikit"; +import { Button, Flex, Icon, Popup, RadioButton, RadioButtonOption, Text, ThemeProvider, Tooltip } from "@gravity-ui/uikit"; import "@gravity-ui/uikit/styles/styles.css"; import React, { useCallback, useLayoutEffect, useRef, useState } from "react"; import { StoryFn } from "storybook/internal/types"; @@ -14,54 +14,59 @@ import { GravityBlock } from "./GravityBlock"; import './Playground.css'; import { TBlock } from "../../components/canvas/blocks/Block"; import { random } from "../../components/canvas/blocks/generate"; +import { MagnifierPlus, MagnifierMinus, SquareDashed, Gear } from '@gravity-ui/icons'; +import { GraphSettings } from "./Settings"; +import { Toolbox } from "./Toolbox"; const generated = generatePlaygroundLayout(0, 5); -export function GraphPLayground() { - const { graph, setEntities, updateEntities, start } = useGraph({ - viewConfiguration: { - colors: { - selection: { - background: "rgba(255, 190, 92, 0.1)", - border: "rgba(255, 190, 92, 1)", - }, - connection: { - background: "rgba(255, 255, 255, 0.5)", - selectedBackground: "rgba(234, 201, 74, 1)", - }, - block: { - background: "rgba(37, 27, 37, 1)", - border: "rgba(229, 229, 229, 0.2)", - selectedBorder: "rgba(255, 190, 92, 1)", - text: "rgba(255, 255, 255, 1)", - }, - anchor: { - background: "rgba(255, 190, 92, 1)" - }, - canvas: { - layerBackground: "rgba(22, 13, 27, 1)", - belowLayerBackground: "rgba(22, 13, 27, 1)", - dots: "rgba(255, 255, 255, 0.2)", - border: "rgba(255, 255, 255, 0.3)", - } - } - }, - settings: { - canDragCamera: true, - canZoomCamera: true, - canDuplicateBlocks: false, - canChangeBlockGeometry: ECanChangeBlockGeometry.ALL, - canCreateNewConnections: false, - showConnectionArrows: false, - scaleFontSize: 1, - useBezierConnections: true, - useBlocksAnchors: true, - showConnectionLabels: false, - blockComponents: { - [GravityBlockIS]: GravityBlock +const config = { + viewConfiguration: { + colors: { + selection: { + background: "rgba(255, 190, 92, 0.1)", + border: "rgba(255, 190, 92, 1)", + }, + connection: { + background: "rgba(255, 255, 255, 0.5)", + selectedBackground: "rgba(234, 201, 74, 1)", + }, + block: { + background: "rgba(37, 27, 37, 1)", + border: "rgba(229, 229, 229, 0.2)", + selectedBorder: "rgba(255, 190, 92, 1)", + text: "rgba(255, 255, 255, 1)", + }, + anchor: { + background: "rgba(255, 190, 92, 1)" + }, + canvas: { + layerBackground: "rgba(22, 13, 27, 1)", + belowLayerBackground: "rgba(22, 13, 27, 1)", + dots: "rgba(255, 255, 255, 0.2)", + border: "rgba(255, 255, 255, 0.3)", } } - }); + }, + settings: { + canDragCamera: true, + canZoomCamera: true, + canDuplicateBlocks: false, + canChangeBlockGeometry: ECanChangeBlockGeometry.ALL, + canCreateNewConnections: false, + showConnectionArrows: false, + scaleFontSize: 1, + useBezierConnections: true, + useBlocksAnchors: true, + showConnectionLabels: false, + blockComponents: { + [GravityBlockIS]: GravityBlock + } + } +}; + +export function GraphPLayground() { + const { graph, setEntities, updateEntities, start } = useGraph(config); const updateVisibleConfig = useFn(() => { const config = graph.rootStore.getAsConfig(); @@ -147,14 +152,16 @@ export function GraphPLayground() { } }, [graph]); - - return ( Graph viewer + + + + diff --git a/src/stories/Playground/Playground.css b/src/stories/Playground/Playground.css index 56d7a6a..78a75eb 100644 --- a/src/stories/Playground/Playground.css +++ b/src/stories/Playground/Playground.css @@ -21,5 +21,58 @@ .view.config-editor { border-radius: 10px; - border-color: var(--g-color-base-float-accent-hover); + border-color: var(--yc-color-base-float-accent-hover); } + +.settings-popup { + padding: 16px; +} + +.graph-tools { + height: 100%; + padding: 20px; + box-sizing: border-box; + position: absolute; + top: 50%; + left: 0; + z-index: 10; + transform: translate(0, -50%); +} + +.button-group .yc-button { + border-radius: 0 !important; +} +.button-group .yc-button::before { + border-radius: 0 !important; +} +.button-group .yc-button::after { + border-radius: 0 !important; +} +.button-group .yc-button:not(:last-child) { + border-bottom: 1px solid var(--yc-color-base-misc-light); +} +.button-group .yc-button:first-child { + border-top-right-radius: var(--yc-button-border-radius, var(--_--border-radius)) !important; + border-top-left-radius: var(--yc-button-border-radius, var(--_--border-radius)) !important; +} +.button-group .yc-button:first-child::before { + border-top-right-radius: var(--yc-button-border-radius, var(--_--border-radius)) !important; + border-top-left-radius: var(--yc-button-border-radius, var(--_--border-radius)) !important; +} +.button-group .yc-button:first-child::after { + border-top-right-radius: var(--yc-button-border-radius, var(--_--border-radius)) !important; + border-top-left-radius: var(--yc-button-border-radius, var(--_--border-radius)) !important; +} +.button-group .yc-button:last-child { + border-bottom-right-radius: var(--yc-button-border-radius, var(--_--border-radius)) !important; + border-bottom-left-radius: var(--yc-button-border-radius, var(--_--border-radius)) !important; +} +.button-group .yc-button:last-child::before { + border-bottom-right-radius: var(--yc-button-border-radius, var(--_--border-radius)) !important; + border-bottom-left-radius: var(--yc-button-border-radius, var(--_--border-radius)) !important; +} +.button-group .yc-button:last-child::after { + border-bottom-right-radius: var(--yc-button-border-radius, var(--_--border-radius)) !important; + border-bottom-left-radius: var(--yc-button-border-radius, var(--_--border-radius)) !important; +} + diff --git a/src/stories/Playground/Settings.tsx b/src/stories/Playground/Settings.tsx new file mode 100644 index 0000000..a3ac7cc --- /dev/null +++ b/src/stories/Playground/Settings.tsx @@ -0,0 +1,60 @@ +import { Gear } from '@gravity-ui/icons'; +import { Button, Flex, Icon, Popup, RadioButton, RadioButtonOption, Text } from "@gravity-ui/uikit"; +import React, { useRef, useState } from "react"; +import { Graph } from '../../graph'; +import { useRerender } from './hooks'; + +const ConnectionVariants: RadioButtonOption[] = [ + { value: 'bezier', content: 'Bezier' }, + { value: 'line', content: 'Line' }, +]; + +const ConnectionArrowsVariants: RadioButtonOption[] = [ + { value: 'bezier', content: 'Show' }, + { value: 'line', content: 'Hide' }, +]; + +export function GraphSettings({graph}: {graph: Graph}) { + + const rerender = useRerender(); + const settingBtnRef = useRef(); + const [settingsOpened, setSettingsOpened] = useState(false); + return <> + + setSettingsOpened(false)} placement={["right-end"]}> + + Graph settings + + Connection type + { + graph.updateSettings({ + useBezierConnections: value === ConnectionVariants[0].value + }); + rerender(); + }} + value={ConnectionVariants[graph.rootStore.settings.getConfigFlag('useBezierConnections') ? 0 : 1].value} + options={ConnectionVariants} + /> + + + Show arrows + { + graph.updateSettings({ + showConnectionArrows: value === ConnectionArrowsVariants[0].value + }); + rerender(); + }} + value={ConnectionArrowsVariants[graph.rootStore.settings.getConfigFlag('showConnectionArrows') ? 0 : 1].value} + options={ConnectionArrowsVariants} + /> + + + + +} \ No newline at end of file diff --git a/src/stories/Playground/Toolbox.tsx b/src/stories/Playground/Toolbox.tsx new file mode 100644 index 0000000..1a4e154 --- /dev/null +++ b/src/stories/Playground/Toolbox.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { Flex, Tooltip, Button, Icon } from "@gravity-ui/uikit"; +import { MagnifierPlus, MagnifierMinus, SquareDashed, Gear } from '@gravity-ui/icons'; +import { Graph } from "../../graph"; +import { useRerender } from './hooks'; + +export function Toolbox({className, graph}: {className: string, graph: Graph}) { + const rerender = useRerender(); + return + + + + + + + + + + +} \ No newline at end of file diff --git a/src/stories/Playground/hooks.ts b/src/stories/Playground/hooks.ts new file mode 100644 index 0000000..14f1d24 --- /dev/null +++ b/src/stories/Playground/hooks.ts @@ -0,0 +1,9 @@ +import { useState } from "react"; +import { useFn } from "../../utils/hooks/useFn"; + +export function useRerender() { + const [tick, setTick] = useState(Date.now()); + return useFn(() => { + setTick(Date.now()); + }) +} \ No newline at end of file From aba7fd68ecdc4c8d19c432e6386199a6e1387845 Mon Sep 17 00:00:00 2001 From: draedful Date: Thu, 10 Oct 2024 03:20:17 +0300 Subject: [PATCH 11/20] playground: prevent select block on click by block's button --- src/stories/Playground/GravityBlock/GravityBlock.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stories/Playground/GravityBlock/GravityBlock.tsx b/src/stories/Playground/GravityBlock/GravityBlock.tsx index d016450..648096f 100644 --- a/src/stories/Playground/GravityBlock/GravityBlock.tsx +++ b/src/stories/Playground/GravityBlock/GravityBlock.tsx @@ -25,7 +25,7 @@ export function PlaygroundBlock({ graph, block }: { graph: Graph, block: TGravit {block.meta.description} - + ) From 9e09b8c5856f838982e418213ab1299ba5412f93 Mon Sep 17 00:00:00 2001 From: draedful Date: Thu, 10 Oct 2024 22:04:35 +0300 Subject: [PATCH 12/20] playground: lot fixes fix: color for line numbers in editor fix: update blocks with multiple anchors fix: appearance of the toolbox and settings button fix: disable zoom-button on reach min/max scale --- src/store/anchor/Anchor.ts | 5 +++ src/store/block/Block.ts | 14 ++++++++ src/stories/Playground/Editor/index.tsx | 2 +- src/stories/Playground/Editor/theme.ts | 38 ++-------------------- src/stories/Playground/GraphPlayground.tsx | 12 +++---- src/stories/Playground/Settings.tsx | 4 +-- src/stories/Playground/Toolbox.tsx | 22 +++++++------ 7 files changed, 41 insertions(+), 56 deletions(-) diff --git a/src/store/anchor/Anchor.ts b/src/store/anchor/Anchor.ts index bf21f5d..a2cafae 100644 --- a/src/store/anchor/Anchor.ts +++ b/src/store/anchor/Anchor.ts @@ -25,6 +25,11 @@ export class AnchorState { ) { this.$state.value = anchor; } + + public update(anchor: TAnchor) { + this.$state.value = anchor; + } + public setSelection(selected: boolean, silent?: boolean) { if (silent) { this.$selected.value = selected; diff --git a/src/store/block/Block.ts b/src/store/block/Block.ts index f2cb829..5f1a4bb 100644 --- a/src/store/block/Block.ts +++ b/src/store/block/Block.ts @@ -3,6 +3,7 @@ import { BlockListStore } from "./BlocksList"; import { Block, TBlock } from "../../components/canvas/blocks/Block"; import { AnchorState } from "../anchor/Anchor"; import { ESelectionStrategy } from "../../utils/types/types"; +import { TAnchor } from "../../components/canvas/anchors"; export type TBlockId = string | number | symbol; @@ -110,8 +111,21 @@ export class BlockState { }; }); + public updateAnchors(anchors: TAnchor[]) { + const anchorsMap = new Map(this.$anchorStates.value.map((a) => [a.id, a])); + this.$anchorStates.value = anchors.filter((a) => a.blockId === this.id).map((anchor) => { + if (anchorsMap.has(anchor.id)) { + const anchorState = anchorsMap.get(anchor.id); + anchorState.update(anchor); + return anchorState; + } + return new AnchorState(this, anchor); + }) + } + public updateBlock(block: Partial): void { this.$state.value = Object.assign({}, this.$state.value, block); + this.updateAnchors(block.anchors); this.getViewComponent()?.updateHitBox(this.$geometry.value, true); } diff --git a/src/stories/Playground/Editor/index.tsx b/src/stories/Playground/Editor/index.tsx index ccf429c..77f09c5 100644 --- a/src/stories/Playground/Editor/index.tsx +++ b/src/stories/Playground/Editor/index.tsx @@ -106,7 +106,7 @@ export const ConfigEditor = React.forwardRef(function ConfigEditor(props: Config theme={GravityTheme} options={{ contextmenu: false, - lineNumbersMinChars: 2, + lineNumbersMinChars: 4, glyphMargin: false, fontSize: 18, lineHeight: 20, diff --git a/src/stories/Playground/Editor/theme.ts b/src/stories/Playground/Editor/theme.ts index 4062655..aff5081 100644 --- a/src/stories/Playground/Editor/theme.ts +++ b/src/stories/Playground/Editor/theme.ts @@ -11,48 +11,14 @@ export function defineTheme(monaco: Monaco) { token: "string.key.json", foreground: "#febe5c", }, - // { - // token: "string.value.json", - // foreground: "#ffffff", - // }, - // { - // token: "number.json", - // foreground: "#ffffff", - // }, - // { - // token: "keyword.json", - // foreground: "#ffffff", - // }, - // { - // token: "delimiter.bracket.json", - // foreground: "#ffffff", - // background: "#ffffff", - // }, - // { - // token: "delimiter.comma.json", - // foreground: "#ffffff", - // background: "#ffffff", - // }, - // { - // token: "delimiter.array.json", - // foreground: "#ffffff", - // background: "#ffffff", - // } ], colors: { "editor.foreground": "#ffdb4d4d", "editor.background": "#251b25", "editor.lineHighlightBackground": "#ffdb4d4d", - "editorLineNumber.foreground": "#251b25", + "editorLineNumber.foreground": "#bd5c0a", "editor.selectionBackground": "#ffdb4d4d", - "editor.inactiveSelectionBackground": "#88000015", - // "editorBracketHighlight.foreground1": "#ffffff", - // "editorBracketHighlight.foreground2": "#ffffff", - // "editorBracketHighlight.foreground3": "#ffffff", - // "editorBracketHighlight.foreground4": "#ffffff", - // "editorBracketHighlight.foreground5": "#ffffff", - // "editorBracketHighlight.foreground6": "#ffffff", - // "editorBracketHighlight.unexpectedBracket.foreground": "#ffffff" + "editor.inactiveSelectionBackground": "#ffdb4d4d", }, }); } \ No newline at end of file diff --git a/src/stories/Playground/GraphPlayground.tsx b/src/stories/Playground/GraphPlayground.tsx index 90fe97d..8df0018 100644 --- a/src/stories/Playground/GraphPlayground.tsx +++ b/src/stories/Playground/GraphPlayground.tsx @@ -1,20 +1,18 @@ -import { Button, Flex, Icon, Popup, RadioButton, RadioButtonOption, Text, ThemeProvider, Tooltip } from "@gravity-ui/uikit"; import "@gravity-ui/uikit/styles/styles.css"; -import React, { useCallback, useLayoutEffect, useRef, useState } from "react"; +import { Flex, Text, ThemeProvider } from "@gravity-ui/uikit"; +import React, { useCallback, useLayoutEffect, useRef } from "react"; import { StoryFn } from "storybook/internal/types"; import { Graph, GraphState } from "../../graph"; import { GraphBlock, GraphCanvas, GraphProps, useGraph, useGraphEvent } from "../../react-component"; import { ECanChangeBlockGeometry } from "../../store/settings"; import { useFn } from "../../utils/hooks/useFn"; -import { PlaygroundBlock } from "./GravityBlock/GravityBlock"; +import { TBlock } from "../../components/canvas/blocks/Block"; +import { random } from "../../components/canvas/blocks/generate"; import { ConfigEditor, ConfigEditorController } from "./Editor"; -import { createPlaygroundBlock, generatePlaygroundLayout, GravityBlockIS, TGravityBlock } from "./generateLayout"; +import { createPlaygroundBlock, generatePlaygroundLayout, GravityBlockIS } from "./generateLayout"; import { GravityBlock } from "./GravityBlock"; import './Playground.css'; -import { TBlock } from "../../components/canvas/blocks/Block"; -import { random } from "../../components/canvas/blocks/generate"; -import { MagnifierPlus, MagnifierMinus, SquareDashed, Gear } from '@gravity-ui/icons'; import { GraphSettings } from "./Settings"; import { Toolbox } from "./Toolbox"; diff --git a/src/stories/Playground/Settings.tsx b/src/stories/Playground/Settings.tsx index a3ac7cc..b84f938 100644 --- a/src/stories/Playground/Settings.tsx +++ b/src/stories/Playground/Settings.tsx @@ -20,7 +20,7 @@ export function GraphSettings({graph}: {graph: Graph}) { const settingBtnRef = useRef(); const [settingsOpened, setSettingsOpened] = useState(false); return <> - setSettingsOpened(false)} placement={["right-end"]}> @@ -41,7 +41,7 @@ export function GraphSettings({graph}: {graph: Graph}) { /> - Show arrows + Show arrows { diff --git a/src/stories/Playground/Toolbox.tsx b/src/stories/Playground/Toolbox.tsx index 1a4e154..0bd3eb6 100644 --- a/src/stories/Playground/Toolbox.tsx +++ b/src/stories/Playground/Toolbox.tsx @@ -1,37 +1,39 @@ -import React from "react"; +import React, { useState } from "react"; import { Flex, Tooltip, Button, Icon } from "@gravity-ui/uikit"; import { MagnifierPlus, MagnifierMinus, SquareDashed, Gear } from '@gravity-ui/icons'; import { Graph } from "../../graph"; -import { useRerender } from './hooks'; +import { useGraphEvent } from "../../react-component"; export function Toolbox({className, graph}: {className: string, graph: Graph}) { - const rerender = useRerender(); + const [scale, setScale] = useState(1); + + useGraphEvent(graph, 'camera-change', ({scale}) => { + setScale(scale) + }) return - - From f398f5e136319c0be6fcdfdb551e7ce39a94123a Mon Sep 17 00:00:00 2001 From: draedful Date: Fri, 11 Oct 2024 02:43:16 +0300 Subject: [PATCH 13/20] playground: lot of fixes --- src/components/canvas/anchors/index.ts | 35 +++++----- src/components/canvas/blocks/Block.ts | 9 ++- .../canvas/layers/graphLayer/GraphLayer.ts | 9 +-- src/react-component/Anchor.tsx | 26 +++---- src/react-component/Block.tsx | 4 ++ .../hooks/useBlockAnchorState.ts | 19 ++++- .../newConnection/ConnectionService.ts | 9 +-- src/services/newConnection/NewConnection.ts | 31 ++++++--- src/store/anchor/Anchor.ts | 4 ++ src/store/block/Block.ts | 18 +++-- src/store/block/BlocksList.ts | 1 - src/stories/Playground/Editor/schema.ts | 2 +- src/stories/Playground/GraphPlayground.tsx | 69 ++++++++++++++++--- .../Playground/GravityBlock/GravityBlock.css | 2 +- src/stories/Playground/GravityBlock/index.tsx | 34 ++++++++- src/stories/Playground/Playground.css | 18 ++++- src/stories/Playground/Settings.tsx | 4 +- src/stories/Playground/generateLayout.tsx | 2 - 18 files changed, 210 insertions(+), 86 deletions(-) diff --git a/src/components/canvas/anchors/index.ts b/src/components/canvas/anchors/index.ts index 53485bc..e50ecd2 100644 --- a/src/components/canvas/anchors/index.ts +++ b/src/components/canvas/anchors/index.ts @@ -1,5 +1,5 @@ import { EventedComponent } from "../../../mixins/withEvents"; -import { withHitTest } from "../../../mixins/withHitTest"; +import { debugHitBox, withHitTest } from "../../../mixins/withHitTest"; import { ECameraScaleLevel } from "../../../services/camera/CameraService"; import { frameDebouncer } from "../../../services/optimizations/frameDebouncer"; import { AnchorState, EAnchorType } from "../../../store/anchor/Anchor"; @@ -13,7 +13,7 @@ export type TAnchor = { id: string; blockId: TBlockId; type: EAnchorType | string; - index: number; + index?: number; }; export type TAnchorProps = TAnchor & { @@ -48,7 +48,7 @@ export class Anchor extends withHitTest(EventedComponent) { private shift: number; - private hitBoxHash: number; + private hitBoxHash: string; private debouncedSetHitBox: (...args: any[]) => void; @@ -102,9 +102,18 @@ export class Anchor extends withHitTest(EventedComponent) { public willIterate() { super.willIterate(); + const { x: poxX, y: posY } = this.props.getPosition(this.props); + const hash = `${poxX}/${posY}/${this.shift}`; + + if (this.hitBoxHash !== hash) { + this.hitBoxHash = hash; + this.debouncedSetHitBox(); + } + const { x, y, width, height } = this.hitBox.getRect(); this.shouldRender = width && height ? this.context.camera.isRectVisible(x, y, width, height) : true; + } public handleEvent(event: MouseEvent | KeyboardEvent) { @@ -115,14 +124,16 @@ export class Anchor extends withHitTest(EventedComponent) { case "click": this.toggleSelected(); break; - case "mouseenter": + case "mouseenter":{ this.setState({ raised: true }); this.computeRenderSize(this.props.size, true); break; - case "mouseleave": + } + case "mouseleave": { this.setState({ raised: false }); this.computeRenderSize(this.props.size, false); break; + } } } @@ -131,18 +142,6 @@ export class Anchor extends withHitTest(EventedComponent) { this.setHitBox(x - this.shift, y - this.shift, x + this.shift, y + this.shift); } - public didRender() { - super.didRender(); - const { x, y } = this.props.getPosition(this.props); - - const hash = x / y + this.shift; - - if (this.hitBoxHash !== hash) { - this.hitBoxHash = hash; - this.debouncedSetHitBox(); - } - } - private computeRenderSize(size: number, raised: boolean) { if (raised) { this.setState({ size: size * 1.8 }); @@ -152,8 +151,6 @@ export class Anchor extends withHitTest(EventedComponent) { } protected render() { - // debugHitBox(this.context.ctx, this); - if (this.context.camera.getCameraBlockScaleLevel() === ECameraScaleLevel.Detailed) { return; } diff --git a/src/components/canvas/blocks/Block.ts b/src/components/canvas/blocks/Block.ts index deb8777..acf63e7 100644 --- a/src/components/canvas/blocks/Block.ts +++ b/src/components/canvas/blocks/Block.ts @@ -187,6 +187,10 @@ export class Block { + this.shouldUpdateChildren = true; + this.performRender(); + }) ]; } @@ -334,10 +338,11 @@ export class Block { this.eventByTargetComponent = event; - const point = this.getTargetPoint(event); + const point = this.context.graph.getPointInCameraSpace(event); this.targetComponent = this.context.graph.getElementOverPoint(point) || this.$.camera; } @@ -269,13 +269,6 @@ export class GraphLayer extends Layer { return this.onRootPointerStart(event); }; - private getTargetPoint(event: MouseEvent) { - const xy = getXY(this.context.canvas, event); - - const applied = this.camera.applyToPoint(xy[0], xy[1]); - return new Point(applied[0], applied[1], { x: xy[0], y: xy[1] }); - } - private onRootPointerStart(event: MouseEvent) { if (event.button === 2 /* Mouse right button */) { /* diff --git a/src/react-component/Anchor.tsx b/src/react-component/Anchor.tsx index 7338979..242991e 100644 --- a/src/react-component/Anchor.tsx +++ b/src/react-component/Anchor.tsx @@ -3,7 +3,7 @@ import { computed } from "@preact/signals-core"; import { TAnchor } from "../components/canvas/anchors"; import { Graph } from "../graph"; import { useSignal } from "./hooks"; -import { useBlockAnchorState } from "./hooks/useBlockAnchorState"; +import { useBlockAnchorPosition, useBlockAnchorState } from "./hooks/useBlockAnchorState"; import { AnchorState } from "../store/anchor/Anchor"; import "./Anchor.css"; @@ -26,22 +26,16 @@ export function GraphBlockAnchor({ const anchorState = useBlockAnchorState(graph, anchor); - useEffect(() => { - if (!container.current) { - return; + useBlockAnchorPosition(anchorState, (position) => { + if (position) { + setCssProps(container.current, { + "--graph-block-anchor-x": `${position.x}px`, + "--graph-block-anchor-y": `${position.y}px`, + }); + } else { + removeCssProps(container.current, ["--graph-block-anchor-x", "--graph-block-anchor-y"]); } - if (position === "absolute") { - const position = anchorState.block.getViewComponent().getAnchorPosition(anchor); - if (position) { - setCssProps(container.current, { - "--graph-block-anchor-x": `${position.x}px`, - "--graph-block-anchor-y": `${position.y}px`, - }); - } else { - removeCssProps(container.current, ["--graph-block-anchor-x", "--graph-block-anchor-y"]); - } - } - }, [container, position, anchorState, anchor]); + }) const selectedSignal = useMemo(() => { return computed(() => { diff --git a/src/react-component/Block.tsx b/src/react-component/Block.tsx index 75df954..39171d5 100644 --- a/src/react-component/Block.tsx +++ b/src/react-component/Block.tsx @@ -42,6 +42,10 @@ export const GraphBlock = ({ } }, [viewState, containerRef]); + if(!viewState || !state) { + return; + } + return (
diff --git a/src/react-component/hooks/useBlockAnchorState.ts b/src/react-component/hooks/useBlockAnchorState.ts index 2a994dc..5831e1e 100644 --- a/src/react-component/hooks/useBlockAnchorState.ts +++ b/src/react-component/hooks/useBlockAnchorState.ts @@ -4,7 +4,8 @@ import { useSignal } from "./useSignal"; import { useBlockState } from "./useBlockState"; import { AnchorState } from "../../store/anchor/Anchor"; import { computed } from "@preact/signals-core"; -import { useMemo } from "react"; +import { useEffect, useMemo, useRef } from "react"; +import debounce from "lodash/debounce"; export function useBlockAnchorState(graph: Graph, anchor: TAnchor): AnchorState | undefined { const blockState = useBlockState(graph, anchor.blockId); @@ -13,3 +14,19 @@ export function useBlockAnchorState(graph: Graph, anchor: TAnchor): AnchorState }, [blockState, anchor]); return useSignal(signal); } + +export function useBlockAnchorPosition(state: AnchorState | undefined, onUpdate: (position: {x: number, y: number}) => void) { + const fnRef = useRef(onUpdate); + fnRef.current = onUpdate; + + useEffect(() => { + if(!state) { + return; + } + return state.block.$geometry.subscribe(debounce(() => { + const position = state.block.getViewComponent().getAnchorPosition(state.state); + fnRef.current(position); + }, 16)) + }, [state.block]) +} + diff --git a/src/services/newConnection/ConnectionService.ts b/src/services/newConnection/ConnectionService.ts index e04c438..9a5c71d 100644 --- a/src/services/newConnection/ConnectionService.ts +++ b/src/services/newConnection/ConnectionService.ts @@ -137,15 +137,10 @@ export class ConnectionService extends Emitter { } public onMoveNewConnection(event: MouseEvent, point: Point) { - this.emit(EVENTS.NEW_CONNECTION_UPDATE, event); - const newTargetComponent = this.graph.getElementOverPoint(point, [Block, Anchor]); + + this.emit(EVENTS.NEW_CONNECTION_UPDATE, { target: newTargetComponent, event }); - if (!(newTargetComponent instanceof Block) || !(newTargetComponent instanceof Anchor)) { - return; - } - - /* Unset selection on move new selection target-point out of components */ if (!newTargetComponent || !newTargetComponent.connectedState) { this.targetComponent?.setSelection(false); this.targetComponent = undefined; diff --git a/src/services/newConnection/NewConnection.ts b/src/services/newConnection/NewConnection.ts index f8d668e..5c87a4c 100644 --- a/src/services/newConnection/NewConnection.ts +++ b/src/services/newConnection/NewConnection.ts @@ -1,3 +1,5 @@ +import { Anchor } from "../../components/canvas/anchors"; +import { Block } from "../../components/canvas/blocks/Block"; import { OverLayer, TOverLayerContext } from "../../components/canvas/layers/overLayer/OverLayer"; import { Component } from "../../lib/Component"; import { getXY } from "../../utils/functions"; @@ -47,13 +49,23 @@ export class NewConnection extends Component { ctx.roundRect(this.state.tx, this.state.ty - 12, 24, 24, 8); ctx.fill(); ctx.fillStyle = this.context.colors.canvas.belowLayerBackground; - renderSVG({ - path: `M7 0.75C7.41421 0.75 7.75 1.08579 7.75 1.5V6.25H12.5C12.9142 6.25 13.25 6.58579 13.25 7C13.25 7.41421 12.9142 7.75 12.5 7.75H7.75V12.5C7.75 12.9142 7.41421 13.25 7 13.25C6.58579 13.25 6.25 12.9142 6.25 12.5V7.75H1.5C1.08579 7.75 0.75 7.41421 0.75 7C0.75 6.58579 1.08579 6.25 1.5 6.25H6.25V1.5C6.25 1.08579 6.58579 0.75 7 0.75Z`, - width: 14, - height: 14, - iniatialWidth: 14, - initialHeight: 14 - }, ctx, { x: this.state.tx, y: this.state.ty - 12, width: 24, height: 24 }) + if(!this.target) { + renderSVG({ + path: `M7 0.75C7.41421 0.75 7.75 1.08579 7.75 1.5V6.25H12.5C12.9142 6.25 13.25 6.58579 13.25 7C13.25 7.41421 12.9142 7.75 12.5 7.75H7.75V12.5C7.75 12.9142 7.41421 13.25 7 13.25C6.58579 13.25 6.25 12.9142 6.25 12.5V7.75H1.5C1.08579 7.75 0.75 7.41421 0.75 7C0.75 6.58579 1.08579 6.25 1.5 6.25H6.25V1.5C6.25 1.08579 6.58579 0.75 7 0.75Z`, + width: 14, + height: 14, + iniatialWidth: 14, + initialHeight: 14 + }, ctx, { x: this.state.tx, y: this.state.ty - 12, width: 24, height: 24 }); + } else { + renderSVG({ + path: `M15.53 1.53A.75.75 0 0 0 14.47.47l-1.29 1.29a4.24 4.24 0 0 0-5.423.483l-.58.58a.96.96 0 0 0 0 1.354l4.646 4.646a.96.96 0 0 0 1.354 0l.58-.58a4.24 4.24 0 0 0 .484-5.423zm-8.5 4.94a.75.75 0 0 1 0 1.06L5.78 8.78l1.44 1.44 1.25-1.25a.75.75 0 0 1 1.06 1.06l-1.25 1.25.543.543a.96.96 0 0 1 0 1.354l-.58.58a4.24 4.24 0 0 1-5.423.484l-1.29 1.29A.75.75 0 0 1 .47 14.47l1.29-1.29a4.24 4.24 0 0 1 .483-5.423l.58-.58a.96.96 0 0 1 1.354 0l.543.543 1.25-1.25a.75.75 0 0 1 1.06 0M3.5 8.62l-.197.197a2.743 2.743 0 0 0 3.879 3.879l.197-.197zm9.197-1.439-.197.197L8.621 3.5l.197-.197a2.743 2.743 0 0 1 3.879 3.879`, + width: 14, + height: 14, + iniatialWidth: 16, + initialHeight: 16 + }, ctx, { x: this.state.tx, y: this.state.ty - 12, width: 24, height: 24 }); + } }) } @@ -66,7 +78,10 @@ export class NewConnection extends Component { }); }; - private updateNewConnectionRender = (event: MouseEvent) => { + protected target?: Block | Anchor; + + private updateNewConnectionRender = ({target, event}: { target?: Block | Anchor, event: MouseEvent }) => { + this.target = target; const xy = getXY(this.context.graphCanvas, event); this.setState({ tx: xy[0], diff --git a/src/store/anchor/Anchor.ts b/src/store/anchor/Anchor.ts index a2cafae..2ba69c5 100644 --- a/src/store/anchor/Anchor.ts +++ b/src/store/anchor/Anchor.ts @@ -19,6 +19,10 @@ export class AnchorState { return this.$state.value.blockId; } + public get state() { + return this.$state.value + } + public constructor( public readonly block: BlockState, anchor: TAnchor diff --git a/src/store/block/Block.ts b/src/store/block/Block.ts index 5f1a4bb..b4282ab 100644 --- a/src/store/block/Block.ts +++ b/src/store/block/Block.ts @@ -40,6 +40,16 @@ export class BlockState { public readonly $anchorStates: Signal = signal([]); + public $anchorIndexs = computed(() => { + const typeIndex = {}; + return new Map(this.$anchorStates.value?.sort((a, b) => ((a.state.index || 0) - (b.state.index || 0)) ).map((anchorState) => { + if (!typeIndex[anchorState.state.type]) { + typeIndex[anchorState.state.type] = 0; + } + return [anchorState.id, typeIndex[anchorState.state.type]++]; + }) || []); + }); + public $anchors = computed(() => { return this.$anchorStates.value?.map((anchorState) => anchorState.asTAnchor()) || []; }); @@ -50,8 +60,6 @@ export class BlockState { private blockView: Block; - public readonly dispose; - public constructor( public readonly store: BlockListStore, block: T @@ -113,7 +121,7 @@ export class BlockState { public updateAnchors(anchors: TAnchor[]) { const anchorsMap = new Map(this.$anchorStates.value.map((a) => [a.id, a])); - this.$anchorStates.value = anchors.filter((a) => a.blockId === this.id).map((anchor) => { + this.$anchorStates.value = anchors.map((anchor) => { if (anchorsMap.has(anchor.id)) { const anchorState = anchorsMap.get(anchor.id); anchorState.update(anchor); @@ -125,7 +133,9 @@ export class BlockState { public updateBlock(block: Partial): void { this.$state.value = Object.assign({}, this.$state.value, block); - this.updateAnchors(block.anchors); + if (block.anchors) { + this.updateAnchors(block.anchors); + } this.getViewComponent()?.updateHitBox(this.$geometry.value, true); } diff --git a/src/store/block/BlocksList.ts b/src/store/block/BlocksList.ts index 3a85b13..e8ccce8 100644 --- a/src/store/block/BlocksList.ts +++ b/src/store/block/BlocksList.ts @@ -327,7 +327,6 @@ export class BlockListStore { const selectedBlocks = this.$selectedBlocks.value; selectedBlocks.forEach((block) => { this.deleteAllBlockConnections(block.id); - block.dispose(); }); const newBlocks = this.$blocks.value.filter((block) => !selectedBlocks.includes(block)); this.applyBlocksState(newBlocks); diff --git a/src/stories/Playground/Editor/schema.ts b/src/stories/Playground/Editor/schema.ts index 37f451f..2df7b84 100644 --- a/src/stories/Playground/Editor/schema.ts +++ b/src/stories/Playground/Editor/schema.ts @@ -146,7 +146,7 @@ export function defineConigSchema(monaco: Monaco) { "description": "Anchor index" } }, - "required": ["id", "blockId", "type", "index"] + "required": ["id", "blockId", "type"] } } }, diff --git a/src/stories/Playground/GraphPlayground.tsx b/src/stories/Playground/GraphPlayground.tsx index 8df0018..71fd573 100644 --- a/src/stories/Playground/GraphPlayground.tsx +++ b/src/stories/Playground/GraphPlayground.tsx @@ -1,6 +1,6 @@ import "@gravity-ui/uikit/styles/styles.css"; import { Flex, Text, ThemeProvider } from "@gravity-ui/uikit"; -import React, { useCallback, useLayoutEffect, useRef } from "react"; +import React, { useCallback, useEffect, useLayoutEffect, useRef } from "react"; import { StoryFn } from "storybook/internal/types"; import { Graph, GraphState } from "../../graph"; import { GraphBlock, GraphCanvas, GraphProps, useGraph, useGraphEvent } from "../../react-component"; @@ -9,6 +9,7 @@ import { useFn } from "../../utils/hooks/useFn"; import { TBlock } from "../../components/canvas/blocks/Block"; import { random } from "../../components/canvas/blocks/generate"; +import { EAnchorType } from "../configurations/definitions"; import { ConfigEditor, ConfigEditorController } from "./Editor"; import { createPlaygroundBlock, generatePlaygroundLayout, GravityBlockIS } from "./generateLayout"; import { GravityBlock } from "./GravityBlock"; @@ -95,20 +96,55 @@ export function GraphPLayground() { ]); }); - useGraphEvent(graph, 'connection-create-drop', ({sourceBlockId, sourceAnchorId, targetBlockId, point}) => { - if(!targetBlockId) { - const block = createPlaygroundBlock(point.x, point.y, graph.rootStore.blocksList.$blocksMap.value.size + 1); - graph.api.addBlock(block); + useGraphEvent(graph, 'connection-created', ({sourceBlockId, sourceAnchorId, targetBlockId, targetAnchorId}, event) => { + event.preventDefault(); + const pullSourceAnchor = graph.rootStore.blocksList.getBlockState(sourceBlockId).getAnchorById(sourceAnchorId); + if (pullSourceAnchor.state.type === EAnchorType.IN) { + graph.api.addConnection({ + sourceBlockId: targetBlockId, + sourceAnchorId: targetAnchorId, + targetBlockId: sourceBlockId, + targetAnchorId: sourceAnchorId, + }) + } else { graph.api.addConnection({ - sourceBlockId, - sourceAnchorId, - targetBlockId: block.id, - targetAnchorId: block.anchors[0].id, + sourceBlockId: sourceBlockId, + sourceAnchorId: sourceAnchorId, + targetBlockId: targetBlockId, + targetAnchorId: targetAnchorId, }) + } + updateVisibleConfig(); + }); + + useGraphEvent(graph, 'connection-create-drop', ({sourceBlockId, sourceAnchorId, targetBlockId, point}) => { + if (targetBlockId) { + return; + } + let block: TBlock; + const pullSourceAnchor = graph.rootStore.blocksList.getBlockState(sourceBlockId).getAnchorById(sourceAnchorId); + if (pullSourceAnchor.state.type === EAnchorType.IN) { + block = createPlaygroundBlock(point.x - 126, point.y - 63, graph.rootStore.blocksList.$blocksMap.value.size + 1); + graph.api.addBlock(block); + graph.api.addConnection({ + sourceBlockId: block.id, + sourceAnchorId: block.anchors[1].id, + targetBlockId: sourceBlockId, + targetAnchorId: sourceAnchorId, + }) + } else { + block = createPlaygroundBlock(point.x, point.y - 63, graph.rootStore.blocksList.$blocksMap.value.size + 1); + graph.api.addBlock(block); + graph.api.addConnection({ + sourceBlockId: sourceBlockId, + sourceAnchorId: sourceAnchorId, + targetBlockId: block.id, + targetAnchorId: block.anchors[0].id, + }) + } graph.zoomTo([block.id], {transition: 250 }); updateVisibleConfig(); editorRef?.current.scrollTo(block.id); - } }); useLayoutEffect(() => { @@ -150,6 +186,17 @@ export function GraphPLayground() { } }, [graph]); + useEffect(() => { + const fn = (e) => { + if (e.code === 'Backspace') { + graph.api.deleteSelected(); + updateVisibleConfig(); + } + }; + document.body.addEventListener('keydown', fn); + return () => document.body.removeEventListener('keydown', fn); + }); + return ( @@ -158,7 +205,7 @@ export function GraphPLayground() { - + diff --git a/src/stories/Playground/GravityBlock/GravityBlock.css b/src/stories/Playground/GravityBlock/GravityBlock.css index 2f55a03..913f7a5 100644 --- a/src/stories/Playground/GravityBlock/GravityBlock.css +++ b/src/stories/Playground/GravityBlock/GravityBlock.css @@ -1,6 +1,6 @@ .gravity-block-wrapper { border-radius: 8px; - border-width: 3px; + border-width: 2px; padding: var(--g-spacing-3); display: flex; diff --git a/src/stories/Playground/GravityBlock/index.tsx b/src/stories/Playground/GravityBlock/index.tsx index 0fbc49b..92afc60 100644 --- a/src/stories/Playground/GravityBlock/index.tsx +++ b/src/stories/Playground/GravityBlock/index.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { CanvasBlock, EAnchorType, layoutText, TAnchor, TBlockId, TPoint } from "../../.."; +import { Anchor, CanvasBlock, EAnchorType, layoutText, TAnchor, TBlockId, TPoint } from "../../.."; import { PlaygroundBlock } from "./GravityBlock"; import { render } from "../../../utils/renderers/render"; @@ -7,6 +7,13 @@ import { renderSVG } from "../../../utils/renderers/svgPath"; import { TGravityBlock } from "../generateLayout"; import './GravityBlock.css'; +function getAnchorY(index) { + let y = 18 * index; + if (index >= 1) { + y += 8 * index; + } + return y + 18; +} export class GravityBlock extends CanvasBlock { @@ -51,10 +58,31 @@ export class GravityBlock extends CanvasBlock { this.context.ctx.strokeRect(this.state.x, this.state.y, this.state.width, this.state.height); } + protected renderAnchor(anchor: TAnchor, getPosition: (anchor: TAnchor) => TPoint) { + return Anchor.create({ + ...anchor, + zIndex: this.zIndex, + size: 18, + lineWidth: 2, + getPosition, + }, { + key: anchor.id + }); + } + + protected getAnchorsYOffter(type: EAnchorType) { + const anchors = this.connectedState.$state.value.anchors.filter((a) => a.type === type); + const { height } = this.getContentRect(); + return (height - getAnchorY(anchors.length - 1)) / 2; + } + public getAnchorPosition(anchor: TAnchor): TPoint { + const a = this.getAnchorsYOffter(anchor.type as EAnchorType); + const index = this.connectedState.$anchorIndexs.value?.get(anchor.id) || 0; + const y = getAnchorY(index); return { x: anchor.type === EAnchorType.OUT ? this.state.width : 0, - y: this.state.height / 2, + y: a + y, }; } @@ -64,7 +92,7 @@ export class GravityBlock extends CanvasBlock { ctx.fillStyle = this.context.colors.block.text; ctx.textAlign = "center"; ctx.textBaseline = "top"; - const { lines, measures, lineHeight } = layoutText(name, ctx, rect, { font: `500 ${9 / scale}px YS Text`, lineHeight: 9 / scale }) + const { lines, measures } = layoutText(name, ctx, rect, { font: `500 ${9 / scale}px YS Text`, lineHeight: 9 / scale }) const shiftY = rect.height / 2 - measures.height / 2; for (let index = 0; index < lines.length; index++) { const [line, x, y] = lines[index]; diff --git a/src/stories/Playground/Playground.css b/src/stories/Playground/Playground.css index 78a75eb..cc83553 100644 --- a/src/stories/Playground/Playground.css +++ b/src/stories/Playground/Playground.css @@ -30,7 +30,14 @@ .graph-tools { height: 100%; - padding: 20px; + pointer-events: none; + position: absolute; + left: 20px; + z-index: 10; +} + +.graph-tools-zoom { + pointer-events: all; box-sizing: border-box; position: absolute; top: 50%; @@ -39,6 +46,15 @@ transform: translate(0, -50%); } +.graph-tools-settings { + pointer-events: all; + box-sizing: border-box; + position: absolute; + bottom: 20px; + left: 0; + z-index: 10; +} + .button-group .yc-button { border-radius: 0 !important; } diff --git a/src/stories/Playground/Settings.tsx b/src/stories/Playground/Settings.tsx index b84f938..08f119b 100644 --- a/src/stories/Playground/Settings.tsx +++ b/src/stories/Playground/Settings.tsx @@ -14,13 +14,13 @@ const ConnectionArrowsVariants: RadioButtonOption[] = [ { value: 'line', content: 'Hide' }, ]; -export function GraphSettings({graph}: {graph: Graph}) { +export function GraphSettings({ className, graph }: {className: string, graph: Graph}) { const rerender = useRerender(); const settingBtnRef = useRef(); const [settingsOpened, setSettingsOpened] = useState(false); return <> - setSettingsOpened(false)} placement={["right-end"]}> diff --git a/src/stories/Playground/generateLayout.tsx b/src/stories/Playground/generateLayout.tsx index 84b8677..280dc93 100644 --- a/src/stories/Playground/generateLayout.tsx +++ b/src/stories/Playground/generateLayout.tsx @@ -26,13 +26,11 @@ export function createPlaygroundBlock(x: number, y: number, index): TGravityBloc id: `${blockId}_anchor_in`, blockId: blockId, type: EAnchorType.IN, - index: 0 }, { id: `${blockId}_anchor_out`, blockId: blockId, type: EAnchorType.OUT, - index: 0 } ], }; From 9e3756f81058c09d05b0b12cabc55cc769d9e1f0 Mon Sep 17 00:00:00 2001 From: draedful Date: Fri, 11 Oct 2024 03:31:30 +0300 Subject: [PATCH 14/20] playground: add examples and some perf improvements --- src/components/canvas/anchors/index.ts | 39 ++++++++------- src/services/newConnection/NewConnection.ts | 3 ++ src/stories/Playground/GraphPlayground.tsx | 50 +++++++++++++++++-- src/stories/Playground/GravityBlock/index.tsx | 9 ++-- src/utils/renderers/svgPath.ts | 19 ++++--- 5 files changed, 85 insertions(+), 35 deletions(-) diff --git a/src/components/canvas/anchors/index.ts b/src/components/canvas/anchors/index.ts index e50ecd2..1c8276e 100644 --- a/src/components/canvas/anchors/index.ts +++ b/src/components/canvas/anchors/index.ts @@ -102,6 +102,14 @@ export class Anchor extends withHitTest(EventedComponent) { public willIterate() { super.willIterate(); + + const { x, y, width, height } = this.hitBox.getRect(); + + this.shouldRender = width && height ? this.context.camera.isRectVisible(x, y, width, height) : true; + + } + + public didIterate(): void { const { x: poxX, y: posY } = this.props.getPosition(this.props); const hash = `${poxX}/${posY}/${this.shift}`; @@ -109,11 +117,6 @@ export class Anchor extends withHitTest(EventedComponent) { this.hitBoxHash = hash; this.debouncedSetHitBox(); } - - const { x, y, width, height } = this.hitBox.getRect(); - - this.shouldRender = width && height ? this.context.camera.isRectVisible(x, y, width, height) : true; - } public handleEvent(event: MouseEvent | KeyboardEvent) { @@ -155,19 +158,17 @@ export class Anchor extends withHitTest(EventedComponent) { return; } const { x, y } = this.props.getPosition(this.props); - render(this.context.ctx, (ctx) => { - ctx.save(); - ctx.fillStyle = this.context.colors.anchor.background; - ctx.beginPath(); - ctx.arc(x, y, this.state.size * 0.5, 0, 2 * Math.PI); - ctx.fill(); - - if (this.state.selected) { - ctx.strokeStyle = this.context.colors.anchor.selectedBorder; - ctx.lineWidth = this.props.lineWidth + 3; - ctx.stroke(); - } - ctx.closePath(); - }) + const ctx = this.context.ctx; + ctx.fillStyle = this.context.colors.anchor.background; + ctx.beginPath(); + ctx.arc(x, y, this.state.size * 0.5, 0, 2 * Math.PI); + ctx.fill(); + + if (this.state.selected) { + ctx.strokeStyle = this.context.colors.anchor.selectedBorder; + ctx.lineWidth = this.props.lineWidth + 3; + ctx.stroke(); + } + ctx.closePath(); } } diff --git a/src/services/newConnection/NewConnection.ts b/src/services/newConnection/NewConnection.ts index 5c87a4c..8069d5a 100644 --- a/src/services/newConnection/NewConnection.ts +++ b/src/services/newConnection/NewConnection.ts @@ -45,6 +45,7 @@ export class NewConnection extends Component { ctx.closePath(); }); render(this.context.ctx, (ctx) => { + ctx.beginPath(); ctx.fillStyle = "rgba(254, 190, 92, 1)"; ctx.roundRect(this.state.tx, this.state.ty - 12, 24, 24, 8); ctx.fill(); @@ -66,6 +67,8 @@ export class NewConnection extends Component { initialHeight: 16 }, ctx, { x: this.state.tx, y: this.state.ty - 12, width: 24, height: 24 }); } + + ctx.closePath(); }) } diff --git a/src/stories/Playground/GraphPlayground.tsx b/src/stories/Playground/GraphPlayground.tsx index 71fd573..4aae928 100644 --- a/src/stories/Playground/GraphPlayground.tsx +++ b/src/stories/Playground/GraphPlayground.tsx @@ -1,8 +1,8 @@ import "@gravity-ui/uikit/styles/styles.css"; -import { Flex, Text, ThemeProvider } from "@gravity-ui/uikit"; +import { Flex, Select, Text, ThemeProvider } from "@gravity-ui/uikit"; import React, { useCallback, useEffect, useLayoutEffect, useRef } from "react"; import { StoryFn } from "storybook/internal/types"; -import { Graph, GraphState } from "../../graph"; +import { Graph, GraphState, TGraphConfig } from "../../graph"; import { GraphBlock, GraphCanvas, GraphProps, useGraph, useGraphEvent } from "../../react-component"; import { ECanChangeBlockGeometry } from "../../store/settings"; import { useFn } from "../../utils/hooks/useFn"; @@ -11,7 +11,7 @@ import { TBlock } from "../../components/canvas/blocks/Block"; import { random } from "../../components/canvas/blocks/generate"; import { EAnchorType } from "../configurations/definitions"; import { ConfigEditor, ConfigEditorController } from "./Editor"; -import { createPlaygroundBlock, generatePlaygroundLayout, GravityBlockIS } from "./generateLayout"; +import { createPlaygroundBlock, generatePlaygroundLayout, GravityBlockIS, TGravityBlock } from "./generateLayout"; import { GravityBlock } from "./GravityBlock"; import './Playground.css'; import { GraphSettings } from "./Settings"; @@ -83,6 +83,7 @@ export function GraphPLayground() { editorRef?.current.updateBlocks([block]); editorRef?.current.scrollTo(block.id); }); + useGraphEvent(graph, "blocks-selection-change", ({ changes }) => { editorRef?.current.updateBlocks([ ...changes.add.map((id) => ({ @@ -197,11 +198,52 @@ export function GraphPLayground() { return () => document.body.removeEventListener('keydown', fn); }); + const updateExample = useFn(([value]) => { + let config: TGraphConfig; + switch(value) { + case "null": { + return; + } + case "1": { + config = generatePlaygroundLayout(0, 5); + break; + } + case "100": { + config = generatePlaygroundLayout(10, 100); + break; + } + case "1000": { + config = generatePlaygroundLayout(23, 150); + break; + } + case "10000": { + graph.updateSettings({ + useBezierConnections: false + }); + config = generatePlaygroundLayout(50, 150); + break; + } + } + console.log(config.blocks.length, config.connections.length) + setEntities(config); + graph.zoomTo('center', {transition: 500}); + updateVisibleConfig(); + }) + return ( - Graph viewer + + Graph viewer + + diff --git a/src/stories/Playground/GravityBlock/index.tsx b/src/stories/Playground/GravityBlock/index.tsx index 92afc60..411899e 100644 --- a/src/stories/Playground/GravityBlock/index.tsx +++ b/src/stories/Playground/GravityBlock/index.tsx @@ -120,9 +120,11 @@ export class GravityBlock extends CanvasBlock { } public renderMinimalisticBlock(ctx: CanvasRenderingContext2D): void { - render(ctx, (ctx) => { this.renderBody(ctx); - ctx.beginPath(); + // do not show icon for large scale + if(this.context.camera.getCameraScale() < 0.1) { + return; + } ctx.fillStyle = "rgba(255, 190, 92, 1)"; renderSVG({ @@ -132,8 +134,6 @@ export class GravityBlock extends CanvasBlock { iniatialWidth: 14, initialHeight: 14, }, ctx, this.getContentRect()); - ctx.closePath(); - }) } public renderDetailedView(ctx: CanvasRenderingContext2D) { @@ -152,6 +152,5 @@ export class GravityBlock extends CanvasBlock { ctx.textAlign = "center"; this.renderTextAtCenter(this.state.name, ctx); } - ctx.closePath(); } } \ No newline at end of file diff --git a/src/utils/renderers/svgPath.ts b/src/utils/renderers/svgPath.ts index a734cb9..70c3eb7 100644 --- a/src/utils/renderers/svgPath.ts +++ b/src/utils/renderers/svgPath.ts @@ -1,3 +1,5 @@ +import { render } from "./render"; + export function renderSVG( icon: { path: string, @@ -10,11 +12,14 @@ export function renderSVG( rect: {x: number, y: number, width: number, height: number}, ) { - const iconPath = new Path2D(icon.path); - const coefX = icon.width / icon.iniatialWidth; - const coefY = icon.height / icon.initialHeight; - // MoveTo position - ctx.translate(rect.x + (rect.width / 2) - (icon.width / 2), rect.y + (rect.height / 2) - (icon.height / 2)); - ctx.scale(coefX, coefY); - ctx.fill(iconPath, 'evenodd'); + render(ctx, (ctx) => { + const iconPath = new Path2D(icon.path); + const coefX = icon.width / icon.iniatialWidth; + const coefY = icon.height / icon.initialHeight; + // MoveTo position + ctx.translate(rect.x + (rect.width / 2) - (icon.width / 2), rect.y + (rect.height / 2) - (icon.height / 2)); + ctx.scale(coefX, coefY); + ctx.fill(iconPath, 'evenodd'); + }) + } From 38ea81c6c1925deb0199ee3c0e2e77ab3359b7b7 Mon Sep 17 00:00:00 2001 From: draedful Date: Fri, 11 Oct 2024 12:22:21 +0300 Subject: [PATCH 15/20] playground: some anchor hooks fix --- src/react-component/Anchor.tsx | 30 +++++++------------ .../hooks/useBlockAnchorState.ts | 15 +++++----- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/react-component/Anchor.tsx b/src/react-component/Anchor.tsx index 242991e..2ee0c8c 100644 --- a/src/react-component/Anchor.tsx +++ b/src/react-component/Anchor.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef } from "react"; +import React, { CSSProperties, useEffect, useMemo, useRef } from "react"; import { computed } from "@preact/signals-core"; import { TAnchor } from "../components/canvas/anchors"; import { Graph } from "../graph"; @@ -22,28 +22,12 @@ export function GraphBlockAnchor({ className?: string; children?: React.ReactNode | ((anchorState: AnchorState) => React.ReactNode); }) { - const container = useRef(null); const anchorState = useBlockAnchorState(graph, anchor); - useBlockAnchorPosition(anchorState, (position) => { - if (position) { - setCssProps(container.current, { - "--graph-block-anchor-x": `${position.x}px`, - "--graph-block-anchor-y": `${position.y}px`, - }); - } else { - removeCssProps(container.current, ["--graph-block-anchor-x", "--graph-block-anchor-y"]); - } - }) + const coords = useBlockAnchorPosition(anchorState) - const selectedSignal = useMemo(() => { - return computed(() => { - return anchorState?.$selected.value; - }); - }, [anchorState]); - - const selected = useSignal(selectedSignal); + const selected = useSignal(anchorState?.$selected); const render = typeof children === "function" ? children : () => children; const classNames = useMemo(() => { @@ -53,7 +37,13 @@ export function GraphBlockAnchor({ }, [anchor, position, className, selected]); return ( -
+
{render(anchorState)}
); diff --git a/src/react-component/hooks/useBlockAnchorState.ts b/src/react-component/hooks/useBlockAnchorState.ts index 5831e1e..e48df1c 100644 --- a/src/react-component/hooks/useBlockAnchorState.ts +++ b/src/react-component/hooks/useBlockAnchorState.ts @@ -4,7 +4,7 @@ import { useSignal } from "./useSignal"; import { useBlockState } from "./useBlockState"; import { AnchorState } from "../../store/anchor/Anchor"; import { computed } from "@preact/signals-core"; -import { useEffect, useMemo, useRef } from "react"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import debounce from "lodash/debounce"; export function useBlockAnchorState(graph: Graph, anchor: TAnchor): AnchorState | undefined { @@ -15,18 +15,19 @@ export function useBlockAnchorState(graph: Graph, anchor: TAnchor): AnchorState return useSignal(signal); } -export function useBlockAnchorPosition(state: AnchorState | undefined, onUpdate: (position: {x: number, y: number}) => void) { - const fnRef = useRef(onUpdate); - fnRef.current = onUpdate; +export function useBlockAnchorPosition(state: AnchorState | undefined) { + const [pos, setPos] = useState<{ x: number, y: number }>(state.block ? state.block.getViewComponent().getAnchorPosition(state.state) : {x: 0, y: 0}); - useEffect(() => { + useLayoutEffect(() => { if(!state) { return; } return state.block.$geometry.subscribe(debounce(() => { const position = state.block.getViewComponent().getAnchorPosition(state.state); - fnRef.current(position); + setPos(position); }, 16)) - }, [state.block]) + }, [state.block]); + + return pos; } From f44085ded63c67bce56a30a47f3c3b954c5a6a9d Mon Sep 17 00:00:00 2001 From: draedful Date: Fri, 11 Oct 2024 14:14:23 +0300 Subject: [PATCH 16/20] playground: fix sizes for small displays --- src/stories/Playground/generateLayout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stories/Playground/generateLayout.tsx b/src/stories/Playground/generateLayout.tsx index 280dc93..10498f0 100644 --- a/src/stories/Playground/generateLayout.tsx +++ b/src/stories/Playground/generateLayout.tsx @@ -14,8 +14,8 @@ export function createPlaygroundBlock(x: number, y: number, index): TGravityBloc is: 'gravity', x, y, - width: 63 * window.devicePixelRatio, - height: 63 * window.devicePixelRatio, + width: 63 * 2, + height: 63 * 2, selected: false, name: `Block ${index}`, meta: { From 2c364eef1367e17c8d15d4161ecc25bd847c53ae Mon Sep 17 00:00:00 2001 From: draedful Date: Tue, 15 Oct 2024 18:35:26 +0300 Subject: [PATCH 17/20] playground some fixes --- src/components/canvas/blocks/Block.ts | 30 +++++++++-------- src/react-component/Block.tsx | 5 +++ src/stories/Playground/GraphPlayground.tsx | 32 +++++++++---------- .../Playground/GravityBlock/GravityBlock.css | 3 +- src/stories/Playground/GravityBlock/index.tsx | 14 +------- src/stories/configurations/generatePretty.ts | 4 +-- 6 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/components/canvas/blocks/Block.ts b/src/components/canvas/blocks/Block.ts index acf63e7..f1b8e1f 100644 --- a/src/components/canvas/blocks/Block.ts +++ b/src/components/canvas/blocks/Block.ts @@ -103,6 +103,8 @@ export class Block this.context.constants.block.SCALES[0]; + public renderSchematicView(ctx: CanvasRenderingContext2D) { + this.renderBody(ctx); - if (shouldRenderText) { + if (this.shouldRenderText) { ctx.fillStyle = this.context.colors.block.text; ctx.textAlign = "center"; this.renderText(this.state.name, ctx); } - if (this.state.selected) { - this.renderStroke(this.context.colors.block.selectedBorder); - } + } - ctx.globalAlpha = 1; + public setHiddenBlock(hidden: boolean) { + if (this.hidden !== hidden) { + this.hidden = hidden; + this.performRender(); + } } // eslint-disable-next-line @typescript-eslint/no-unused-vars - public renderDetailedView(_ctx: CanvasRenderingContext2D) { - return; + public renderDetailedView(ctx: CanvasRenderingContext2D) { + return this.renderBody(ctx); } protected render() { diff --git a/src/react-component/Block.tsx b/src/react-component/Block.tsx index 39171d5..e085d9c 100644 --- a/src/react-component/Block.tsx +++ b/src/react-component/Block.tsx @@ -22,6 +22,11 @@ export const GraphBlock = ({ const viewState = useBlockViewState(graph, block); const state = useBlockState(graph, block); + useEffect(() => { + viewState?.setHiddenBlock(true); + return () => viewState?.setHiddenBlock(false); + }, [viewState]); + useLayoutEffect(() => { setCssProps(containerRef.current, { "--graph-block-geometry-x": `${block.x}px`, diff --git a/src/stories/Playground/GraphPlayground.tsx b/src/stories/Playground/GraphPlayground.tsx index 4aae928..e409ad0 100644 --- a/src/stories/Playground/GraphPlayground.tsx +++ b/src/stories/Playground/GraphPlayground.tsx @@ -3,7 +3,7 @@ import { Flex, Select, Text, ThemeProvider } from "@gravity-ui/uikit"; import React, { useCallback, useEffect, useLayoutEffect, useRef } from "react"; import { StoryFn } from "storybook/internal/types"; import { Graph, GraphState, TGraphConfig } from "../../graph"; -import { GraphBlock, GraphCanvas, GraphProps, useGraph, useGraphEvent } from "../../react-component"; +import { GraphBlock, GraphCanvas, GraphProps, HookGraphParams, useGraph, useGraphEvent } from "../../react-component"; import { ECanChangeBlockGeometry } from "../../store/settings"; import { useFn } from "../../utils/hooks/useFn"; @@ -19,7 +19,7 @@ import { Toolbox } from "./Toolbox"; const generated = generatePlaygroundLayout(0, 5); -const config = { +const config: HookGraphParams = { viewConfiguration: { colors: { selection: { @@ -45,6 +45,11 @@ const config = { dots: "rgba(255, 255, 255, 0.2)", border: "rgba(255, 255, 255, 0.3)", } + }, + constants: { + block: { + SCALES: [0.1, 0.2, 0.5] + } } }, settings: { @@ -66,6 +71,7 @@ const config = { export function GraphPLayground() { const { graph, setEntities, updateEntities, start } = useGraph(config); + const editorRef = useRef(null); const updateVisibleConfig = useFn(() => { const config = graph.rootStore.getAsConfig(); @@ -73,11 +79,7 @@ export function GraphPLayground() { blocks: config.blocks || [], connections: config.connections || [] }); - }) - - useGraphEvent(graph, 'block-drag', ({block}) => { - editorRef?.current.scrollTo(block.id); - }) + }); useGraphEvent(graph, 'block-change', ({block}) => { editorRef?.current.updateBlocks([block]); @@ -179,13 +181,11 @@ export function GraphPLayground() { return Unknown block <>{block.id} }); - const editorRef = useRef(null); - - const onSelectBlock: GraphProps['onBlockSelectionChange'] = useCallback((selection) => { - if (selection.list.length === 1) { - editorRef?.current.scrollTo(selection.list[0]); + useGraphEvent(graph, 'blocks-selection-change', ({list}) => { + if (list.length === 1) { + editorRef?.current.scrollTo(list[0]); } - }, [graph]); + }); useEffect(() => { const fn = (e) => { @@ -224,11 +224,11 @@ export function GraphPLayground() { break; } } - console.log(config.blocks.length, config.connections.length) setEntities(config); graph.zoomTo('center', {transition: 500}); updateVisibleConfig(); - }) + + }); return ( @@ -249,7 +249,7 @@ export function GraphPLayground() { - + diff --git a/src/stories/Playground/GravityBlock/GravityBlock.css b/src/stories/Playground/GravityBlock/GravityBlock.css index 913f7a5..1eb1cd7 100644 --- a/src/stories/Playground/GravityBlock/GravityBlock.css +++ b/src/stories/Playground/GravityBlock/GravityBlock.css @@ -1,6 +1,6 @@ .gravity-block-wrapper { border-radius: 8px; - border-width: 2px; + border-width: 4px; padding: var(--g-spacing-3); display: flex; @@ -14,6 +14,7 @@ .gravity-block-wrapper:hover { cursor: pointer; + border-color: rgba(229, 229, 229, 0.4); background-color: rgba(57, 47, 57, 1); } diff --git a/src/stories/Playground/GravityBlock/index.tsx b/src/stories/Playground/GravityBlock/index.tsx index 411899e..1e1de59 100644 --- a/src/stories/Playground/GravityBlock/index.tsx +++ b/src/stories/Playground/GravityBlock/index.tsx @@ -51,13 +51,6 @@ export class GravityBlock extends CanvasBlock { return } - protected renderStroke(color: string) { - const scale = this.context.camera.getCameraScale(); - this.context.ctx.lineWidth = Math.min(Math.round(3 / scale), 12); - this.context.ctx.strokeStyle = color; - this.context.ctx.strokeRect(this.state.x, this.state.y, this.state.width, this.state.height); - } - protected renderAnchor(anchor: TAnchor, getPosition: (anchor: TAnchor) => TPoint) { return Anchor.create({ ...anchor, @@ -104,7 +97,7 @@ export class GravityBlock extends CanvasBlock { public renderBody(ctx: CanvasRenderingContext2D) { const scale = this.context.camera.getCameraScale(); - ctx.lineWidth = Math.min(Math.round(3 / scale), 12); + ctx.lineWidth = Math.min(Math.round(2 / scale), 12); ctx.fillStyle = this.hovered ? "rgba(57, 47, 57, 1)" : this.context.colors.block.background; ctx.beginPath(); @@ -136,11 +129,6 @@ export class GravityBlock extends CanvasBlock { }, ctx, this.getContentRect()); } - public renderDetailedView(ctx: CanvasRenderingContext2D) { - // This needs to prevent flickering of block on switch levels - this.renderBody(ctx); - } - public renderSchematicView(ctx: CanvasRenderingContext2D) { this.renderBody(ctx); diff --git a/src/stories/configurations/generatePretty.ts b/src/stories/configurations/generatePretty.ts index 1fb206e..601f05c 100644 --- a/src/stories/configurations/generatePretty.ts +++ b/src/stories/configurations/generatePretty.ts @@ -11,8 +11,8 @@ function createBlock(x: number, y: number, index): TBlock { is: IS_BLOCK_TYPE, x, y, - width: 63 * window.devicePixelRatio, - height: 63 * window.devicePixelRatio, + width: 200, + height: 150, selected: false, name: blockId, anchors: [], From 26c3c3ef4388bfdac599d140bccf07e8f03e99e3 Mon Sep 17 00:00:00 2001 From: evtaranov Date: Wed, 16 Oct 2024 20:47:35 +0300 Subject: [PATCH 18/20] feat(playground): bigger buttons, graph size, css tune --- src/stories/Playground/Editor/index.tsx | 22 +++++----- src/stories/Playground/GraphPlayground.tsx | 47 ++++++++++++---------- src/stories/Playground/Playground.css | 16 +++++++- src/stories/Playground/Settings.tsx | 8 +++- src/stories/Playground/Toolbox.tsx | 13 +++--- 5 files changed, 65 insertions(+), 41 deletions(-) diff --git a/src/stories/Playground/Editor/index.tsx b/src/stories/Playground/Editor/index.tsx index 77f09c5..1376764 100644 --- a/src/stories/Playground/Editor/index.tsx +++ b/src/stories/Playground/Editor/index.tsx @@ -1,16 +1,16 @@ -import { Button, Flex, Icon, Text } from "@gravity-ui/uikit"; -import { Editor, loader, OnMount, OnValidate } from "@monaco-editor/react"; +import { Button, Flex, Text } from "@gravity-ui/uikit"; +import { Editor, OnMount, OnValidate, loader } from "@monaco-editor/react"; import React, { Ref, useImperativeHandle, useRef, useState } from "react"; import type { TBlock } from "../../../components/canvas/blocks/Block"; import { TBlockId } from "../../../store/block/Block"; import type { TConnection } from "../../../store/connection/ConnectionState"; -import { defineTheme, GravityTheme } from "./theme"; +import { GravityTheme, defineTheme } from "./theme"; import "./Editor.css"; -import { findBlockPositionsMonaco } from "./utils"; import { defineConigSchema } from "./schema"; +import { findBlockPositionsMonaco } from "./utils"; loader.init().then((monaco) => { @@ -24,12 +24,12 @@ export interface ConfigEditorController { setContent: (p: {blocks: TBlock[], connections: TConnection[]}) => void; } -type ConfigEditorProps = { +type ConfigEditorProps = { onChange?: (config: { blocks: TBlock[], connections: TConnection[] }) => void addBlock?: () => void }; -type ExtractTypeFromArray = T extends Array ? E : never; +type ExtractTypeFromArray = T extends Array ? E : never; export const ConfigEditor = React.forwardRef(function ConfigEditor(props: ConfigEditorProps, ref: Ref) { @@ -46,7 +46,7 @@ export const ConfigEditor = React.forwardRef(function ConfigEditor(props: Config } const model = monacoRef.current.getModel(); const range = findBlockPositionsMonaco(model, blockId); - + if (range?.start.column) { monacoRef.current?.revealLinesInCenter(range.start.lineNumber, range.end.lineNumber, 0); } @@ -76,7 +76,7 @@ export const ConfigEditor = React.forwardRef(function ConfigEditor(props: Config text: text.slice(19, text.length - 6), } }) - + model.applyEdits(edits); }, setContent: ({ blocks, connections}) => { @@ -119,7 +119,7 @@ export const ConfigEditor = React.forwardRef(function ConfigEditor(props: Config /> - - + {errorMarker && ( { monacoRef.current?.revealLinesInCenter(errorMarker.startLineNumber, errorMarker.endLineNumber, 0); - + monacoRef.current.setSelection({ startColumn: errorMarker.startColumn, diff --git a/src/stories/Playground/GraphPlayground.tsx b/src/stories/Playground/GraphPlayground.tsx index e409ad0..5683534 100644 --- a/src/stories/Playground/GraphPlayground.tsx +++ b/src/stories/Playground/GraphPlayground.tsx @@ -1,9 +1,9 @@ +import { Flex, RadioButton, RadioButtonOption, RadioButtonProps, Text, ThemeProvider } from "@gravity-ui/uikit"; import "@gravity-ui/uikit/styles/styles.css"; -import { Flex, Select, Text, ThemeProvider } from "@gravity-ui/uikit"; -import React, { useCallback, useEffect, useLayoutEffect, useRef } from "react"; +import React, { useEffect, useLayoutEffect, useRef } from "react"; import { StoryFn } from "storybook/internal/types"; import { Graph, GraphState, TGraphConfig } from "../../graph"; -import { GraphBlock, GraphCanvas, GraphProps, HookGraphParams, useGraph, useGraphEvent } from "../../react-component"; +import { GraphBlock, GraphCanvas, HookGraphParams, useGraph, useGraphEvent } from "../../react-component"; import { ECanChangeBlockGeometry } from "../../store/settings"; import { useFn } from "../../utils/hooks/useFn"; @@ -69,6 +69,13 @@ const config: HookGraphParams = { } }; +const graphSizeOptions: RadioButtonOption[] = [ + {value: '1', content: 'S'}, + {value: '100', content: 'M'}, + {value: '1000', content: 'L'}, + {value: '10000', content: 'XL'}, +]; + export function GraphPLayground() { const { graph, setEntities, updateEntities, start } = useGraph(config); const editorRef = useRef(null); @@ -198,25 +205,22 @@ export function GraphPLayground() { return () => document.body.removeEventListener('keydown', fn); }); - const updateExample = useFn(([value]) => { + const updateGraphSize = useFn, void>((value) => { let config: TGraphConfig; switch(value) { - case "null": { - return; - } - case "1": { + case graphSizeOptions[0].value: { config = generatePlaygroundLayout(0, 5); break; } - case "100": { + case graphSizeOptions[1].value: { config = generatePlaygroundLayout(10, 100); break; } - case "1000": { + case graphSizeOptions[2].value: { config = generatePlaygroundLayout(23, 150); break; } - case "10000": { + case graphSizeOptions[3].value: { graph.updateSettings({ useBezierConnections: false }); @@ -227,22 +231,21 @@ export function GraphPLayground() { setEntities(config); graph.zoomTo('center', {transition: 500}); updateVisibleConfig(); - + }); return ( - - Graph viewer - + + Graph + @@ -255,7 +258,7 @@ export function GraphPLayground() { JSON Editor - { updateEntities({blocks, connections}) diff --git a/src/stories/Playground/Playground.css b/src/stories/Playground/Playground.css index cc83553..96f1258 100644 --- a/src/stories/Playground/Playground.css +++ b/src/stories/Playground/Playground.css @@ -16,7 +16,7 @@ border: 1px solid transparent; border-radius: 24px; overflow: hidden; - position: relative + position: relative; } .view.config-editor { @@ -46,6 +46,20 @@ transform: translate(0, -50%); } +.graph-size-settings { + --g-color-base-brand: #FFBE5C; + --g-color-base-background: var(--g-color-base-brand); + --g-color-text-primary: var(--g-color-text-dark-primary); + + .yc-radio-button__option:hover .yc-radio-button__option-text { + --g-color-text-primary: var(--g-color-text-light-primary); + } + + .yc-radio-button__option_checked.yc-radio-button__option:hover .yc-radio-button__option-text { + --g-color-text-primary: var(--g-color-text-dark-primary); + } +} + .graph-tools-settings { pointer-events: all; box-sizing: border-box; diff --git a/src/stories/Playground/Settings.tsx b/src/stories/Playground/Settings.tsx index 08f119b..7b1c8b7 100644 --- a/src/stories/Playground/Settings.tsx +++ b/src/stories/Playground/Settings.tsx @@ -20,7 +20,13 @@ export function GraphSettings({ className, graph }: {className: string, graph: G const settingBtnRef = useRef(); const [settingsOpened, setSettingsOpened] = useState(false); return <> - setSettingsOpened(false)} placement={["right-end"]}> diff --git a/src/stories/Playground/Toolbox.tsx b/src/stories/Playground/Toolbox.tsx index 0bd3eb6..4ac35c0 100644 --- a/src/stories/Playground/Toolbox.tsx +++ b/src/stories/Playground/Toolbox.tsx @@ -1,6 +1,6 @@ +import { MagnifierMinus, MagnifierPlus, SquareDashed } from '@gravity-ui/icons'; +import { Button, Flex, Icon, Tooltip } from "@gravity-ui/uikit"; import React, { useState } from "react"; -import { Flex, Tooltip, Button, Icon } from "@gravity-ui/uikit"; -import { MagnifierPlus, MagnifierMinus, SquareDashed, Gear } from '@gravity-ui/icons'; import { Graph } from "../../graph"; import { useGraphEvent } from "../../react-component"; @@ -11,8 +11,9 @@ export function Toolbox({className, graph}: {className: string, graph: Graph}) { setScale(scale) }) return - + - - - + {errorMarker && ( diff --git a/src/stories/Playground/GraphPlayground.tsx b/src/stories/Playground/GraphPlayground.tsx index c9dd6b8..767c932 100644 --- a/src/stories/Playground/GraphPlayground.tsx +++ b/src/stories/Playground/GraphPlayground.tsx @@ -207,7 +207,7 @@ export function GraphPLayground() { }); useEffect(() => { - const fn = (e) => { + const fn = (e: KeyboardEvent) => { if (e.code === 'Backspace') { graph.api.deleteSelected(); updateVisibleConfig();