From 6634dea19a6e520bee8aebcfd2216b4e0fdafece Mon Sep 17 00:00:00 2001 From: draedful Date: Tue, 8 Oct 2024 21:32:32 +0300 Subject: [PATCH] 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 | 4 +- 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, 885 insertions(+), 102 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..ebe22ac 100644 --- a/src/graph.ts +++ b/src/graph.ts @@ -29,9 +29,9 @@ 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; 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; }