diff --git a/src/factories/bar.ts b/src/factories/bar.ts new file mode 100644 index 00000000..9e27856b --- /dev/null +++ b/src/factories/bar.ts @@ -0,0 +1,83 @@ +import { ColorSource, Container } from 'pixi.js' +import { capFactory } from '@/factories/cap' +import { rectangleFactory } from '@/factories/rectangle' + +type BarStyle = { + width: number, + height: number, + background: ColorSource, + radius: number, + capLeft?: false, + capRight?: false, +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export async function barFactory() { + const bar = new Container() + const rectangle = await rectangleFactory() + const { left, right, render: renderCaps } = await capFactory() + + bar.addChild(rectangle) + bar.addChild(left) + bar.addChild(right) + + async function render(style: BarStyle): Promise { + const { width, x, visible } = getRectangleStyles(style) + + await renderCaps(style) + + rectangle.visible = visible + rectangle.width = width + rectangle.height = style.height + rectangle.x = x + + left.visible = getCapVisibility(style.capLeft, style.radius) + right.visible = getCapVisibility(style.capRight, style.radius) + + right.x = style.radius + width + + rectangle.tint = style.background + left.tint = style.background + right.tint = style.background + + return bar + } + + function getCapVisibility(enabled: boolean | undefined, radius: number): boolean { + if (radius === 0) { + return false + } + + return enabled ?? true + } + + function getRectangleStyles(style: BarStyle): { width: number, x: number, visible: boolean } { + const left = getCapVisibility(style.capLeft, style.radius) + const right = getCapVisibility(style.capRight, style.radius) + + let caps = 0 + + if (left) { + caps += style.radius + } + + if (right) { + caps += style.radius + } + + const width = Math.max(style.width - caps, 0) + const visible = width > 0 + const x = left ? style.radius : 0 + + return { + width, + visible, + x, + } + } + + return { + bar, + render, + } +} \ No newline at end of file diff --git a/src/factories/box.ts b/src/factories/box.ts deleted file mode 100644 index 223ab0a0..00000000 --- a/src/factories/box.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { differenceInMilliseconds, millisecondsInSecond } from 'date-fns' -import { Graphics } from 'pixi.js' -import { DEFAULT_LINEAR_COLUMN_SIZE_PIXELS, DEFAULT_TIME_COLUMN_SIZE_PIXELS } from '@/consts' -import { RunGraphNode } from '@/models/RunGraph' -import { waitForConfig } from '@/objects/config' -import { layout } from '@/objects/layout' - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export async function nodeBoxFactory() { - const config = await waitForConfig() - const box = new Graphics() - - async function render(node: RunGraphNode): Promise { - const { background } = config.styles.node(node) - - const width = getWidth(node) - const height = config.styles.nodeHeight - config.styles.nodeMargin * 2 - - box.clear() - box.lineStyle(1, 0x0, 1, 2) - box.beginFill(background) - box.drawRoundedRect(0, 0, width, height, 4) - box.endFill() - - return await box - } - - function getWidth(node: RunGraphNode): number { - if (layout.horizontal === 'trace') { - const right = node.start_time - const left = node.end_time ?? new Date() - const seconds = differenceInMilliseconds(left, right) / millisecondsInSecond - const width = seconds * DEFAULT_TIME_COLUMN_SIZE_PIXELS - - return width - } - - return DEFAULT_LINEAR_COLUMN_SIZE_PIXELS - } - - return { - box, - render, - } -} \ No newline at end of file diff --git a/src/factories/cap.ts b/src/factories/cap.ts new file mode 100644 index 00000000..f53a24f2 --- /dev/null +++ b/src/factories/cap.ts @@ -0,0 +1,58 @@ +import { useSubscription } from '@prefecthq/vue-compositions' +import { Graphics, Rectangle, Sprite, Texture } from 'pixi.js' +import { waitForApplication } from '@/objects' + +type CapStyle = { + height: number, + radius: number, +} + +async function cap({ height, radius }: CapStyle): Promise { + const application = await waitForApplication() + + const graphic = new Graphics() + graphic.beginFill('#fff') + graphic.drawRoundedRect(0, 0, radius * 2, height, radius) + graphic.endFill() + + const cap = application.renderer.generateTexture(graphic, { + // drew a rounded rectangle and then just using half of the graphic to get just the left "cap" + region: new Rectangle(0, 0, radius, height), + + // manually bumping up the resolution to keep the border radius from being blurry + resolution: 10, + }) + + return cap +} + +type CapSprites = { + left: Sprite, + right: Sprite, +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function capFactory() { + const left = new Sprite() + const right = new Sprite() + + async function render(style: CapStyle): Promise { + const texture = (await useSubscription(cap, [style]).promise()).response + left.texture = texture + right.texture = texture + + right.anchor.x = 1 + right.scale.x = -1 + + return { + left, + right, + } + } + + return { + left, + right, + render, + } +} \ No newline at end of file diff --git a/src/factories/node.ts b/src/factories/node.ts index 5f3396a5..19cae9a7 100644 --- a/src/factories/node.ts +++ b/src/factories/node.ts @@ -1,6 +1,6 @@ import { Container, Ticker } from 'pixi.js' -import { FlowRunContainer, flowRunContainerFactory } from '@/factories/flowRun' -import { TaskRunContainer, taskRunContainerFactory } from '@/factories/taskRun' +import { FlowRunContainer, flowRunContainerFactory } from '@/factories/nodeFlowRun' +import { TaskRunContainer, taskRunContainerFactory } from '@/factories/nodeTaskRun' import { RunGraphNode } from '@/models/RunGraph' export type NodeContainerFactory = Awaited> diff --git a/src/factories/nodeBar.ts b/src/factories/nodeBar.ts new file mode 100644 index 00000000..1822678b --- /dev/null +++ b/src/factories/nodeBar.ts @@ -0,0 +1,49 @@ +import { differenceInMilliseconds, millisecondsInSecond } from 'date-fns' +import { Container } from 'pixi.js' +import { DEFAULT_LINEAR_COLUMN_SIZE_PIXELS, DEFAULT_TIME_COLUMN_SIZE_PIXELS } from '@/consts' +import { barFactory } from '@/factories/bar' +import { RunGraphNode } from '@/models/RunGraph' +import { waitForConfig } from '@/objects/config' +import { layout } from '@/objects/layout' + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export async function nodeBarFactory() { + const config = await waitForConfig() + const { bar, render: renderBar } = await barFactory() + + async function render(node: RunGraphNode): Promise { + const { background = '#fff' } = config.styles.node(node) + const { nodeHeight: height, nodeBorderRadius: radius } = config.styles + const width = getTotalWidth(node, radius) + const capLeft = node.state_type !== 'running' + + await renderBar({ + width, + height, + radius, + background, + capLeft, + }) + + return bar + } + + function getTotalWidth(node: RunGraphNode, borderRadius: number): number { + if (layout.horizontal === 'trace') { + const right = node.start_time + const left = node.end_time ?? new Date() + const seconds = differenceInMilliseconds(left, right) / millisecondsInSecond + const width = seconds * DEFAULT_TIME_COLUMN_SIZE_PIXELS + + // this means the min node size is 18px. Is that correct? + return Math.max(width, borderRadius * 2) + } + + return DEFAULT_LINEAR_COLUMN_SIZE_PIXELS + } + + return { + bar, + render, + } +} \ No newline at end of file diff --git a/src/factories/flowRun.ts b/src/factories/nodeFlowRun.ts similarity index 78% rename from src/factories/flowRun.ts rename to src/factories/nodeFlowRun.ts index 3a561ed0..c3285f68 100644 --- a/src/factories/flowRun.ts +++ b/src/factories/nodeFlowRun.ts @@ -1,7 +1,7 @@ -import { BitmapText, Container, Graphics } from 'pixi.js' +import { BitmapText, Container } from 'pixi.js' import { DEFAULT_NODE_CONTAINER_NAME } from '@/consts' -import { nodeBoxFactory } from '@/factories/box' import { nodeLabelFactory } from '@/factories/label' +import { nodeBarFactory } from '@/factories/nodeBar' import { nodesContainerFactory } from '@/factories/nodes' import { Pixels } from '@/models/layout' import { RunGraphNode } from '@/models/RunGraph' @@ -13,13 +13,13 @@ export type FlowRunContainer = Awaited { const label = await renderLabel(node) - const box = await renderBox(node) + const bar = await renderBar(node) - label.position = getLabelPosition(label, box) + label.position = getLabelPosition(label, bar) return container } @@ -54,11 +54,11 @@ export async function flowRunContainerFactory(node: RunGraphNode) { } } - function getLabelPosition(label: BitmapText, box: Graphics): Pixels { + function getLabelPosition(label: BitmapText, bar: Container): Pixels { // todo: this should probably be nodePadding const margin = config.styles.nodeMargin - const inside = box.width > margin + label.width + margin - const y = box.height / 2 - label.height + const inside = bar.width > margin + label.width + margin + const y = bar.height / 2 - label.height if (inside) { return { @@ -68,7 +68,7 @@ export async function flowRunContainerFactory(node: RunGraphNode) { } return { - x: box.width + margin, + x: bar.width + margin, y, } } diff --git a/src/factories/taskRun.ts b/src/factories/nodeTaskRun.ts similarity index 68% rename from src/factories/taskRun.ts rename to src/factories/nodeTaskRun.ts index acb056f0..daa21239 100644 --- a/src/factories/taskRun.ts +++ b/src/factories/nodeTaskRun.ts @@ -1,7 +1,7 @@ -import { BitmapText, Container, Graphics } from 'pixi.js' +import { BitmapText, Container } from 'pixi.js' import { DEFAULT_NODE_CONTAINER_NAME } from '@/consts' -import { nodeBoxFactory } from '@/factories/box' import { nodeLabelFactory } from '@/factories/label' +import { nodeBarFactory } from '@/factories/nodeBar' import { Pixels } from '@/models/layout' import { RunGraphNode } from '@/models/RunGraph' import { waitForConfig } from '@/objects/config' @@ -12,9 +12,9 @@ export type TaskRunContainer = Awaited { const label = await renderLabel(node) - const box = await renderBox(node) + const bar = await renderBar(node) - label.position = await getLabelPosition(label, box) + label.position = await getLabelPosition(label, bar) return container } - async function getLabelPosition(label: BitmapText, box: Graphics): Promise { + async function getLabelPosition(label: BitmapText, bar: Container): Promise { const config = await waitForConfig() // todo: this should probably be nodePadding const margin = config.styles.nodeMargin - const inside = box.width > margin + label.width + margin - const y = box.height / 2 - label.height + const inside = bar.width > margin + label.width + margin + const y = bar.height / 2 - label.height if (inside) { return { @@ -45,7 +45,7 @@ export async function taskRunContainerFactory() { } return { - x: box.width + margin, + x: bar.width + margin, y, } } diff --git a/src/factories/rectangle.ts b/src/factories/rectangle.ts new file mode 100644 index 00000000..9973a92c --- /dev/null +++ b/src/factories/rectangle.ts @@ -0,0 +1,22 @@ +import { useSubscription } from '@prefecthq/vue-compositions' +import { Graphics, RenderTexture, Sprite } from 'pixi.js' +import { waitForApplication } from '@/objects' + +async function rectangle(): Promise { + const application = await waitForApplication() + + const rectangle = new Graphics() + rectangle.beginFill('#fff') + rectangle.drawRect(0, 0, 1, 1) + rectangle.endFill() + + const texture = application.renderer.generateTexture(rectangle) + + return texture +} + +export async function rectangleFactory(): Promise { + const texture = (await useSubscription(rectangle).promise()).response + + return new Sprite(texture) +} \ No newline at end of file diff --git a/src/models/RunGraph.ts b/src/models/RunGraph.ts index 89e92fca..17e697df 100644 --- a/src/models/RunGraph.ts +++ b/src/models/RunGraph.ts @@ -50,6 +50,7 @@ export type RunGraphNodeStyles = { export type RunGraphStyles = { nodeHeight?: number, nodeMargin?: number, + nodeBorderRadius?: number, node?: (node: RunGraphNode) => RunGraphNodeStyles, } diff --git a/src/objects/config.ts b/src/objects/config.ts index cd13af61..11d20552 100644 --- a/src/objects/config.ts +++ b/src/objects/config.ts @@ -11,6 +11,7 @@ const defaults: Omit = { styles: { nodeHeight: 30, nodeMargin: 2, + nodeBorderRadius: 8, node: () => ({ background: '#ffffff', }),