From a73c9c1d2c7abd48e328cce9c349cb4f1bfdc572 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Wed, 27 Sep 2023 13:28:08 -0500 Subject: [PATCH 01/10] Clean up the demo --- demo/.eslintrc.json | 5 + demo/App.vue | 99 ++--- .../AppComponentNavigationItems.vue | 40 ++ demo/components/AppNavigationBar.vue | 109 +++++ demo/components/ComponentPage.vue | 151 ------- demo/components/ContextSidebar.vue | 120 ------ demo/components/HashLink.vue | 54 --- demo/components/ResizableSection.vue | 129 ------ demo/components/ResizeIcon.svg | 9 - demo/components/contextAccordionChildItem.ts | 6 - demo/components/router.ts | 17 - demo/main.ts | 2 +- demo/router/index.ts | 7 +- demo/router/routeRecords.ts | 3 +- demo/router/routeRecordsFlat.ts | 3 +- demo/sections/Data.vue | 85 ---- demo/sections/Home.vue | 113 ------ demo/sections/WelcomePage.vue | 117 ++++++ demo/sections/components/FlowRunGraphDemo.vue | 5 + .../components/FlowRunTimelineDemo.vue | 375 ------------------ demo/sections/components/TimescaleTable.vue | 122 ------ demo/sections/components/index.ts | 3 +- demo/style.css | 5 - src/styles/style.css | 2 + 24 files changed, 327 insertions(+), 1254 deletions(-) create mode 100644 demo/.eslintrc.json create mode 100644 demo/components/AppComponentNavigationItems.vue create mode 100644 demo/components/AppNavigationBar.vue delete mode 100644 demo/components/ComponentPage.vue delete mode 100644 demo/components/ContextSidebar.vue delete mode 100644 demo/components/HashLink.vue delete mode 100644 demo/components/ResizableSection.vue delete mode 100644 demo/components/ResizeIcon.svg delete mode 100644 demo/components/contextAccordionChildItem.ts delete mode 100644 demo/components/router.ts delete mode 100644 demo/sections/Data.vue delete mode 100644 demo/sections/Home.vue create mode 100644 demo/sections/WelcomePage.vue create mode 100644 demo/sections/components/FlowRunGraphDemo.vue delete mode 100644 demo/sections/components/FlowRunTimelineDemo.vue delete mode 100644 demo/sections/components/TimescaleTable.vue delete mode 100644 demo/style.css create mode 100644 src/styles/style.css diff --git a/demo/.eslintrc.json b/demo/.eslintrc.json new file mode 100644 index 00000000..3c9990d7 --- /dev/null +++ b/demo/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-relative-import-paths/no-relative-import-paths": "off" + } +} \ No newline at end of file diff --git a/demo/App.vue b/demo/App.vue index 8e95638b..b4340b37 100644 --- a/demo/App.vue +++ b/demo/App.vue @@ -1,42 +1,29 @@ + + \ No newline at end of file diff --git a/src/FlowRunTimeline.vue b/src/FlowRunTimeline.vue deleted file mode 100644 index c70b2eb7..00000000 --- a/src/FlowRunTimeline.vue +++ /dev/null @@ -1,469 +0,0 @@ - - - - - diff --git a/src/components/FlowRunGraph.vue b/src/components/FlowRunGraph.vue new file mode 100644 index 00000000..59ecfbf6 --- /dev/null +++ b/src/components/FlowRunGraph.vue @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 00000000..6650e331 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1 @@ +export { default as FlowRunGraph } from './FlowRunGraph.vue' diff --git a/src/containers/guide.ts b/src/containers/guide.ts deleted file mode 100644 index 186074c8..00000000 --- a/src/containers/guide.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Viewport } from 'pixi-viewport' -import { Application, BitmapText, Container, Sprite } from 'pixi.js' -import { watch } from 'vue' -import { GraphState, ParsedThemeStyles } from '@/models/FlowRunTimeline' -import { getBitmapFonts, getSimpleFillTexture } from '@/pixiFunctions' -import { ViewportUpdatedCheck, viewportUpdatedFactory } from '@/utilities/viewport' - -export type GuideDateFormatter = (value: Date) => string - -export type GuidesArgs = { - application: Application, - viewport: Viewport, - styles: ParsedThemeStyles, -} - -export class Guide extends Container { - - private readonly state: GraphState - private readonly viewportUpdated: ViewportUpdatedCheck - private format: GuideDateFormatter | undefined - private date: Date | undefined - private line: Sprite | undefined - private label: BitmapText | undefined - private readonly unwatch: ReturnType - - public constructor(state: GraphState) { - super() - - this.state = state - this.viewportUpdated = viewportUpdatedFactory(state.viewport) - - this.state.pixiApp.ticker.add(this.tick) - - this.interactive = false - - this.createLine() - this.createLabel() - - this.unwatch = watch(state.styleOptions, () => { - this.createLine() - this.createLabel() - }) - } - - public setDate(value: Date): void { - const updated = value.getTime() !== this.date?.getTime() - - this.date = value - - if (updated) { - this.updatePosition() - this.updateLabel() - } - - } - - public setFormat(value: GuideDateFormatter): void { - this.format = value - - this.updateLabel() - } - - public destroy(): void { - this.state.pixiApp.ticker.remove(this.tick) - - super.destroy.call(this) - } - - private readonly tick = (): void => { - if (!this.date || !this.viewportUpdated()) { - return - } - - this.updatePosition() - this.updateLineHeight() - } - - private createLine(): void { - const texture = getSimpleFillTexture({ - pixiApp: this.state.pixiApp, - fill: this.state.styleOptions.value.colorGuideLine, - }) - - this.line = new Sprite(texture) - this.line.width = 1 - this.line.height = this.state.pixiApp.screen.height - this.addChild(this.line) - } - - private async createLabel(): Promise { - const fonts = await getBitmapFonts(this.state.styleOptions.value) - this.label = new BitmapText('', fonts.timeMarkerLabel) - - const padding = this.state.styleOptions.value.spacingGuideLabelPadding - this.label.position.set(padding, padding) - - this.addChild(this.label) - - this.updateLabel() - } - - private getPositionX(): number { - if (!this.date) { - throw new Error('Guide position requested for undefined date') - } - - const { viewport } = this.state - - return this.state.timeScale.dateToX(this.date) * viewport.scale._x + viewport.worldTransform.tx - } - - private updateLineHeight(): void { - if (!this.line) { - return - } - - this.line.height = this.state.pixiApp.screen.height - } - - private updatePosition(): void { - const x = this.getPositionX() - - this.position.set(x, 0) - } - - private updateLabel(): void { - if (!this.format || !this.date || !this.label) { - return - } - - this.label.text = this.format(this.date) - } -} \ No newline at end of file diff --git a/src/containers/guides.ts b/src/containers/guides.ts deleted file mode 100644 index a2934ec6..00000000 --- a/src/containers/guides.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { addMilliseconds } from 'date-fns' -import { Viewport } from 'pixi-viewport' -import { Application, Container } from 'pixi.js' -import { watch } from 'vue' -import { Guide, GuideDateFormatter } from '@/containers/guide' -import { FormatDateFns, GraphState, ParsedThemeStyles } from '@/models/FlowRunTimeline' -import { TimeSpan, getLabelFormatter, getTimeSpanSlot } from '@/utilities' -import { ViewportUpdatedCheck, viewportUpdatedFactory } from '@/utilities/viewport' - -export type GuidesArgs = { - application: Application, - viewport: Viewport, - styles: ParsedThemeStyles, - formatters: FormatDateFns, -} - -type ViewportDates = { - startDate: Date, - endDate: Date, - span: number, -} - -const MAX_GUIDES = 20 -const GUIDE_GAP_PIXELS = 260 -const VIEWPORT_BUFFER = 100 - -export class Guides extends Container { - private readonly state: GraphState - private readonly guides: Guide[] = [] - private readonly viewportUpdated: ViewportUpdatedCheck - private readonly unwatch: ReturnType - - public constructor(state: GraphState) { - super() - - this.state = state - - this.viewportUpdated = viewportUpdatedFactory(state.viewport) - - this.state.pixiApp.ticker.add(this.tick) - - this.unwatch = watch([this.state.styleOptions, this.state.formatDateFns], () => { - this.updateGuides() - }) - } - - public destroy(): void { - this.state.pixiApp.ticker.remove(this.tick) - this.unwatch() - - super.destroy.call(this) - } - - private readonly tick = (): void => { - if (!this.viewportUpdated()) { - return - } - - this.updateGuides() - } - - private getViewportDates(): ViewportDates { - const startDate = this.state.timeScale.xToDate(this.state.viewport.left - VIEWPORT_BUFFER) - const endDate = this.state.timeScale.xToDate(this.state.viewport.right + VIEWPORT_BUFFER) - const span = endDate.getTime() - startDate.getTime() - - return { startDate, endDate, span } - } - - private getTimeSpan(): TimeSpan { - const width = this.state.pixiApp.screen.width + VIEWPORT_BUFFER * 2 - const numberOfGuides = Math.ceil(width / GUIDE_GAP_PIXELS) - const { span } = this.getViewportDates() - const guideSpan = Math.ceil(span / numberOfGuides) - - return getTimeSpanSlot(guideSpan) - } - - private getFirstGuideDate(startDate: Date, span: number): Date { - const firstGuideTime = Math.ceil(startDate.getTime() / span) * span - - return new Date(firstGuideTime) - } - - private getGuideDates(): Date[] { - const { startDate, endDate } = this.getViewportDates() - const { span } = this.getTimeSpan() - const dates: Date[] = [] - - let date = this.getFirstGuideDate(startDate, span) - - if (date.getTime() >= endDate.getTime()) { - return dates - } - - while (date.getTime() < endDate.getTime()) { - dates.push(date) - date = addMilliseconds(date, span) - } - - while (dates.length > MAX_GUIDES) { - dates.shift() - dates.pop() - } - - return dates - } - - private getLabelFormat(): GuideDateFormatter { - const { labelFormat } = this.getTimeSpan() - - return getLabelFormatter(labelFormat, this.state.formatDateFns.value) - } - - private updateGuides(): void { - const dates = this.getGuideDates() - const unused = this.guides.splice(dates.length) - - dates.forEach((date, index) => this.updateOrCreateGuide(index, date)) - - this.removeGuides(unused) - } - - private updateOrCreateGuide(index: number, date: Date): Guide { - const existing = this.guides.at(index) - const guide = existing ?? new Guide(this.state) - - guide.setDate(date) - guide.setFormat(this.getLabelFormat()) - - this.guides[index] = guide - - if (!existing) { - this.addChild(guide) - } - - return guide - } - - private removeGuides(guides: Guide[]): void { - guides.forEach(guide => { - guide.destroy() - this.removeChild(guide) - }) - } - -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 4df4c4c0..6a4bef18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,2 @@ -export { default as FlowRunTimeline } from './FlowRunTimeline.vue' -export * from './types' +export * from './components' export * from './models' diff --git a/src/models/FlowRunTimeline.ts b/src/models/FlowRunTimeline.ts deleted file mode 100644 index bb4ef40f..00000000 --- a/src/models/FlowRunTimeline.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { Cull } from '@pixi-essentials/cull' -import type { Viewport } from 'pixi-viewport' -import type { Application, IBitmapTextStyle, TextStyle } from 'pixi.js' -import type { ComputedRef } from 'vue' -import { TimelineData, TimelineItem } from '@/types/timeline' -import { formatDate, formatDateByMinutes, formatDateBySeconds } from '@/utilities' - -export type TimeScaleArgs = { - minimumStartTime: number, - graphXDomain: number, - initialOverallTimeSpan: number, -} - -export type TimelineVisibleDateRange = { - startDate: Date, - endDate: Date, - internalOrigin?: boolean, -} - -export type TimelineNodesLayoutOptions = 'waterfall' | 'nearestParent' - -export type NodeSelectionEventTypes = 'task' | 'subFlowRun' -export type NodeSelectionEvent = { - id: string, - type: NodeSelectionEventTypes, -} - -export type ExpandedSubNodes = Record> = Map, - // user may define anything else to be used externally -} & T> - -export type NodeShoveDirection = 1 | -1 - -export type NodeLayoutWorkerArgs = { - layoutSetting?: TimelineNodesLayoutOptions, - data?: TimelineData, - apxCharacterWidth?: number, - spacingMinimumNodeEdgeGap?: number, - timeScaleArgs?: TimeScaleArgs, - centerViewportAfter?: boolean, -} -export type NodeLayoutItem = { - row: number, - nextDependencyShove?: NodeShoveDirection, - startX: number, - endX: number, -} -export type NodesLayout = Record - -export type NodeLayoutRow = { yPos: number, height: number } - -export type NodeLayoutWorkerResponseData = { - layout: NodesLayout, - centerViewportAfter?: boolean, -} -export type NodeLayoutWorkerResponse = { - data: NodeLayoutWorkerResponseData, -} - -export type GraphState = { - pixiApp: Application, - viewport: Viewport, - cull: Cull, - cullScreen: () => void, - timeScale: TimeScale, - timeScaleArgs: TimeScaleArgs, - styleOptions: ComputedRef, - styleNode: ComputedRef, - layoutSetting: ComputedRef, - formatDateFns: ComputedRef, - isRunning: ComputedRef, - hideEdges: ComputedRef, - subNodeLabels: ComputedRef>, - selectedNodeId: ComputedRef, - expandedSubNodes: ComputedRef, - suppressMotion: ComputedRef, - centerViewport: (options?: CenterViewportOptions) => void, -} - -export type DateToX = (date: Date) => number -export type XToDate = (xPosition: number) => Date -export type TimeScale = { - dateToX: DateToX, - xToDate: XToDate, -} - -export type CenterViewportOptions = { - skipAnimation?: boolean, -} - -export type TextStyles = { - nodeTextDefault: Partial, - nodeTextInverse: Partial, - nodeTextSubdued: Partial, - nodeTextStyles: TextStyle, - timeMarkerLabel: Partial, - playheadTimerLabel: Partial, -} - -type FormatDate = (date: Date) => string - -export type FormatDateFns = { - timeBySeconds: FormatDate, - timeByMinutes: FormatDate, - date: FormatDate, -} -export const formatDateFnsDefault: FormatDateFns = { - timeBySeconds: formatDateBySeconds, - timeByMinutes: formatDateByMinutes, - date: formatDate, -} - -type NodeThemeOptions = { - fill: string, - // If your fill is a dark color, set inverse to true. Unless using dark mode text colors. - inverseTextOnFill: boolean, - // for the SubNode toggle buttons - onFillSubNodeToggleHoverBg: string, - onFillSubNodeToggleHoverBgAlpha: number, -} -export type NodeThemeFn = (node: TimelineItem) => NodeThemeOptions -export const nodeThemeFnDefault: NodeThemeFn = () => { - return { - fill: 'black', - inverseTextOnFill: true, - onFillSubNodeToggleHoverBg: 'black', - onFillSubNodeToggleHoverBgAlpha: 0.4, - } -} - -export type Sizing = `${string}px` | `${string}em` | `${string}rem` - -export type RGB = `rgb(${number}, ${number}, ${number})` -export type RGBA = `rgba(${number}, ${number}, ${number}, ${number})` -export type HSL = `hsl(${string | number}, ${number | string}, ${number | string})` -export type HEX = `#${string}` -export type Color = RGB | RGBA | HSL | HEX - -export type ThemeStyleOverrides = { - colorTextDefault?: Color, - colorTextInverse?: Color, - colorTextSubdued?: Color, - colorNodeSelection?: Color, - colorButtonBg?: Color, - colorButtonBgHover?: Color, - colorButtonBorder?: Color | null, - colorEdge?: Color, - colorGuideLine?: Color, - colorPlayheadBg?: Color, - textFontFamilyDefault?: string, - textSizeDefault?: Sizing, - textSizeSmall?: Sizing, - textLineHeightDefault?: Sizing, - textLineHeightSmall?: Sizing, - spacingButtonBorderWidth?: Sizing, - spacingViewportPaddingDefault?: Sizing, - spacingNodeXPadding?: Sizing, - spacingNodeYPadding?: Sizing, - spacingNodeMargin?: Sizing, - spacingNodeLabelMargin?: Sizing, - spacingMinimumNodeEdgeGap?: Sizing, - spacingNodeEdgeLength?: Sizing, - spacingNodeSelectionMargin?: Sizing, - spacingNodeSelectionWidth?: Sizing, - spacingSubNodesOutlineBorderWidth?: Sizing, - spacingSubNodesOutlineOffset?: Sizing, - spacingEdgeWidth?: Sizing, - spacingGuideLabelPadding?: Sizing, - spacingPlayheadWidth?: Sizing, - spacingPlayheadGlowPadding?: Sizing, - borderRadiusNode?: Sizing, - borderRadiusButton?: Sizing, - alphaNodeDimmed?: number, - alphaSubNodesOutlineDimmed?: number, -} - -export type ParsedThemeStyles = { - colorTextDefault: number, - colorTextInverse: number, - colorTextSubdued: number, - colorNodeSelection: number, - colorButtonBg: number, - colorButtonBgHover: number, - colorButtonBorder: number | null, - colorEdge: number, - colorGuideLine: number, - colorPlayheadBg: number, - textFontFamilyDefault: string, - textSizeDefault: number, - textSizeSmall: number, - textLineHeightDefault: number, - textLineHeightSmall: number, - spacingButtonBorderWidth: number, - spacingViewportPaddingDefault: number, - spacingNodeXPadding: number, - spacingNodeYPadding: number, - spacingNodeMargin: number, - spacingNodeLabelMargin: number, - spacingMinimumNodeEdgeGap: number, - spacingNodeEdgeLength: number, - spacingNodeSelectionMargin: number, - spacingNodeSelectionWidth: number, - spacingSubNodesOutlineBorderWidth: number, - spacingSubNodesOutlineOffset: number, - spacingEdgeWidth: number, - spacingGuideLabelPadding: number, - spacingPlayheadWidth: number, - spacingPlayheadGlowPadding: number, - borderRadiusNode: number, - borderRadiusButton: number, - alphaNodeDimmed: number, - alphaSubNodesOutlineDimmed: number, -} - -export type TimelineThemeOptions = { - node?: NodeThemeFn, - defaults?: ThemeStyleOverrides, -} - -export type ParsedThemeOptions = { - node: NodeThemeFn, - defaults: ParsedThemeStyles, -} diff --git a/src/models/TimelineGraph.ts b/src/models/TimelineGraph.ts new file mode 100644 index 00000000..3eddc5a0 --- /dev/null +++ b/src/models/TimelineGraph.ts @@ -0,0 +1,7 @@ +export type TimelineGraph = { + +} + +export type TimelineGraphNode = { + +} \ No newline at end of file diff --git a/src/models/index.ts b/src/models/index.ts index 0baa5202..568f3c2b 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1 +1 @@ -export * from './FlowRunTimeline' +export * from './TimelineGraph' diff --git a/src/pixiFunctions/bitmapFonts.ts b/src/pixiFunctions/bitmapFonts.ts deleted file mode 100644 index 076e845a..00000000 --- a/src/pixiFunctions/bitmapFonts.ts +++ /dev/null @@ -1,174 +0,0 @@ -import FontFaceObserver from 'fontfaceobserver' -import { BitmapFont, IBitmapTextStyle, TextStyle } from 'pixi.js' -import { TextStyles, ParsedThemeStyles } from '@/models' - -let bitmapFontsFontFamily = 'sans-serif' -let bitmapFontsCache: Promise | null = null - -const nodeTextStyles = new TextStyle() -const baseOptions = { - chars: BitmapFont.ASCII, -} -const timelineMarkerStyles = new TextStyle() - -async function loadBitmapFonts(styles: ParsedThemeStyles): Promise { - const font = new FontFaceObserver(styles.textFontFamilyDefault) - - try { - await font.load() - } catch (error) { - console.error(error) - console.warn(`loadBitmapFonts: font ${styles.textFontFamilyDefault} failed to load, falling back to ${bitmapFontsFontFamily}`) - return createBitmapFonts(bitmapFontsFontFamily, styles) - } - - bitmapFontsFontFamily = styles.textFontFamilyDefault - return createBitmapFonts(bitmapFontsFontFamily, styles) -} - -function assignTextStyles(styles: ParsedThemeStyles): void { - nodeTextStyles.fontFamily = bitmapFontsFontFamily - nodeTextStyles.fontSize = styles.textSizeDefault - nodeTextStyles.lineHeight = styles.textLineHeightDefault - - timelineMarkerStyles.fontFamily = bitmapFontsFontFamily - timelineMarkerStyles.fontSize = styles.textSizeSmall - timelineMarkerStyles.lineHeight = styles.textLineHeightSmall -} - -function createBitmapFonts(fontFamily: string, styles: ParsedThemeStyles): TextStyles { - assignTextStyles(styles) - - const options = { - resolution: window.devicePixelRatio || 2, - ...baseOptions, - } - - BitmapFont.from( - 'NodeTextDefault', - { - ...nodeTextStyles, - fill: styles.colorTextDefault, - }, options, - ) - const nodeTextDefault: Partial = { - fontName: 'NodeTextDefault', - fontSize: styles.textSizeDefault, - } - - BitmapFont.from( - 'NodeTextInverse', - { - ...nodeTextStyles, - fill: styles.colorTextInverse, - }, options, - ) - const nodeTextInverse: Partial = { - fontName: 'NodeTextInverse', - fontSize: styles.textSizeDefault, - } - - BitmapFont.from( - 'NodeTextSubdued', - { - ...nodeTextStyles, - fill: styles.colorTextSubdued, - }, options, - ) - const nodeTextSubdued: Partial = { - fontName: 'NodeTextSubdued', - fontSize: styles.textSizeDefault, - } - - BitmapFont.from( - 'TimeMarkerLabel', - { - ...timelineMarkerStyles, - fill: styles.colorTextSubdued, - }, options, - ) - const timeMarkerLabel: Partial = { - fontName: 'TimeMarkerLabel', - fontSize: styles.textSizeSmall, - } - - BitmapFont.from( - 'PlayheadTimerLabel', - { - ...timelineMarkerStyles, - fill: styles.colorPlayheadBg, - }, options, - ) - const playheadTimerLabel: Partial = { - fontName: 'PlayheadTimerLabel', - fontSize: styles.textSizeSmall, - } - - return { - nodeTextDefault, - nodeTextInverse, - nodeTextSubdued, - nodeTextStyles, - timeMarkerLabel, - playheadTimerLabel, - } -} - -export function updateBitmapFonts(styles: ParsedThemeStyles): void { - assignTextStyles(styles) - - const options = { - resolution: window.devicePixelRatio || 2, - ...baseOptions, - } - - setTimeout(() => { - BitmapFont.uninstall('NodeTextDefault') - BitmapFont.from( - 'NodeTextDefault', - { - ...nodeTextStyles, - fill: styles.colorTextDefault, - }, options, - ) - - BitmapFont.uninstall('NodeTextInverse') - BitmapFont.from( - 'NodeTextInverse', - { - ...nodeTextStyles, - fill: styles.colorTextInverse, - }, options, - ) - - BitmapFont.uninstall('TimeMarkerLabel') - BitmapFont.from( - 'TimeMarkerLabel', - { - ...timelineMarkerStyles, - fill: styles.colorTextSubdued, - }, options, - ) - - BitmapFont.uninstall('PlayheadTimerLabel') - BitmapFont.from( - 'PlayheadTimerLabel', - { - ...timelineMarkerStyles, - fill: styles.colorPlayheadBg, - }, options, - ) - }, 0) -} - -export const getBitmapFonts = (styles: ParsedThemeStyles): Promise => { - if (!bitmapFontsCache) { - bitmapFontsCache = loadBitmapFonts(styles) - } - - return bitmapFontsCache -} - -export const initBitmapFonts = (styles: ParsedThemeStyles): void => { - bitmapFontsCache = loadBitmapFonts(styles) -} diff --git a/src/pixiFunctions/deselectLayer.ts b/src/pixiFunctions/deselectLayer.ts deleted file mode 100644 index a4233104..00000000 --- a/src/pixiFunctions/deselectLayer.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { Viewport } from 'pixi-viewport' -import { Application, Container, Graphics, UPDATE_PRIORITY } from 'pixi.js' - -export class DeselectLayer extends Container { - private readonly appRef: Application - private readonly viewportRef: Viewport - private readonly layer = new Graphics() - - public constructor(appRef: Application, viewportRef: Viewport) { - super() - this.appRef = appRef - this.viewportRef = viewportRef - - this.drawLayer() - - this.initUpdater() - - this.interactive = true - } - - private drawLayer(): void { - const { xPos, yPos, width, height } = this.getDimensions() - - this.layer.beginFill(0xFFFFFF, 1) - this.layer.drawRect(xPos, yPos, width, height) - this.layer.endFill() - - // workaround for pixi not rendering graphics with no fill or stroke - // zero alpha fill is not rendered at first - // read more here: https://github.com/pixijs/pixijs/issues/5614 - this.layer.alpha = 0 - - this.addChild(this.layer) - } - - private getDimensions(): { xPos: number, yPos: number, width: number, height: number } { - return { - xPos: this.viewportRef.left, - yPos: this.viewportRef.top, - width: this.viewportRef.right - this.viewportRef.left, - height: this.viewportRef.bottom - this.viewportRef.top, - } - } - - private initUpdater(): void { - this.appRef.ticker.add(() => { - this.update() - }, null, UPDATE_PRIORITY.LOW) - } - - private update(): void { - const { xPos, yPos, width, height } = this.getDimensions() - this.x = xPos - this.y = yPos - this.width = width - this.height = height - } -} diff --git a/src/pixiFunctions/index.ts b/src/pixiFunctions/index.ts deleted file mode 100644 index bebcda0e..00000000 --- a/src/pixiFunctions/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * from './bitmapFonts' -export * from './deselectLayer' -export * from './initPixiApp' -export * from './initViewport' -export * from './loadingIndicator' -export * from './nodeSprites' -export * from './roundedBorderRect' -export * from './timelineEdge' -export * from './timelineNode' -export * from './timelineNodes' -export * from './timelinePlayhead' -export * from './timeScale' -export * from './subNodesToggle' diff --git a/src/pixiFunctions/initPixiApp.ts b/src/pixiFunctions/initPixiApp.ts deleted file mode 100644 index 3611964f..00000000 --- a/src/pixiFunctions/initPixiApp.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Application, UPDATE_PRIORITY, utils } from 'pixi.js' - -export function initPixiApp(stage: HTMLElement): Application { - utils.skipHello() - - const pixiApp = new Application({ - backgroundAlpha: 0, - resolution: window.devicePixelRatio || 2, - autoDensity: true, - antialias: true, - }) - - if (process.env.NODE_ENV === 'development') { - initPixiDevTools(pixiApp) - } - - stage.appendChild(pixiApp.view) - - pixiApp.ticker.add(() => { - if (stage.clientWidth !== pixiApp.screen.width || stage.clientHeight !== pixiApp.screen.height) { - pixiApp.resizeTo = stage - } - }, null, UPDATE_PRIORITY.LOW) - - return pixiApp -} - -function initPixiDevTools(app: Application): void { - // @ts-expect-error - Pixi dev tools are not in the types - // eslint-disable-next-line no-undef - globalThis.__PIXI_APP__ = app -} diff --git a/src/pixiFunctions/initViewport.ts b/src/pixiFunctions/initViewport.ts deleted file mode 100644 index 6596d7eb..00000000 --- a/src/pixiFunctions/initViewport.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { Viewport as ViewportType } from 'pixi-viewport' -import { Application, UPDATE_PRIORITY } from 'pixi.js' -import { nextTick } from 'vue' -import { getPixiViewport } from '@/pixiFunctions/viewport' -import { zIndex } from '@/utilities' - -export async function initViewport(stage: HTMLElement, appRef: Application): Promise { - const Viewport = await getPixiViewport() - const { width, height } = appRef.screen - - const viewport = new Viewport({ - screenWidth: width, - screenHeight: height, - passiveWheel: false, - interaction: appRef.renderer.plugins.interaction, - divWheel: stage, - ticker: appRef.ticker, - }) - - viewport.zIndex = zIndex.viewport - - viewport - .drag({ - wheel: false, - pressDrag: true, - }) - .wheel({ - trackpadPinch: true, - wheelZoom: true, - }) - .pinch() - .clampZoom({ - minWidth: width / 2, - }) - .decelerate({ - friction: 0.9, - }) - - appRef.stage.addChild(viewport) - - // Resize viewport when screen size changes - appRef.ticker.add(() => { - if (viewport.screenWidth !== appRef.screen.width || viewport.screenHeight !== appRef.screen.height) { - viewport.resize(appRef.screen.width, appRef.screen.height) - - // the resize event must be called on next tick so the viewport has a chance to update - nextTick(() => { - viewport.emit('resize') - }) - } - }, null, UPDATE_PRIORITY.LOW) - - return viewport -} diff --git a/src/pixiFunctions/loadingIndicator.ts b/src/pixiFunctions/loadingIndicator.ts deleted file mode 100644 index 4a6708d5..00000000 --- a/src/pixiFunctions/loadingIndicator.ts +++ /dev/null @@ -1,121 +0,0 @@ -import gsap from 'gsap' -import { Container, Sprite, Graphics } from 'pixi.js' -import { WatchStopHandle, watch } from 'vue' -import { GraphState } from '@/models' - -const yPadding = 16 -const dotSize = 8 -const dotsGap = 4 -const dotAnimationDuration = 0.7 -const dotAnimationOffset = 0.25 - -type LoadingIndicatorProps = { - graphState: GraphState, -} - -export class LoadingIndicator extends Container { - private readonly graphState - - private readonly dimensionsObject = new Sprite() - private dots: Graphics[] = [] - - private readonly unWatchers: WatchStopHandle[] = [] - - public constructor({ - graphState, - }: LoadingIndicatorProps) { - super() - - this.graphState = graphState - - this.initDimensions() - this.initDots() - - this.initWatchers() - } - - private initWatchers(): void { - const { styleOptions } = this.graphState - const styleWatcher = watch(styleOptions, () => { - this.destroyDots() - this.initDots() - }) - - this.unWatchers.push(styleWatcher) - } - - private initDimensions(): void { - const width = dotSize * 3 + dotsGap * 2 - const height = yPadding * 2 + dotSize - - this.dimensionsObject.width = width - this.dimensionsObject.height = height - - this.addChild(this.dimensionsObject) - } - - private initDots(): void { - const { colorTextDefault } = this.graphState.styleOptions.value - - const dot = new Graphics() - - dot.beginFill(colorTextDefault) - dot.drawCircle(0, 0, dotSize / 2) - dot.endFill() - dot.alpha = 0 - dot.position.y = yPadding - - const dot2 = dot.clone() - dot2.alpha = 0 - dot2.position.set(dotSize + dotsGap, yPadding) - - const dot3 = dot.clone() - dot3.alpha = 0 - dot3.position.set((dotSize + dotsGap) * 2, yPadding) - - this.dots.push(dot, dot2, dot3) - this.addChild(dot, dot2, dot3) - - this.initAnimation() - } - - private initAnimation(): void { - const animateAlpha = (el: Graphics, delay: number = 0): void => { - gsap.to(el, { - alpha: 1, - duration: dotAnimationDuration, - delay: delay, - onComplete: () => { - gsap.to(el, { - alpha: 0, - duration: dotAnimationDuration, - onComplete: () => animateAlpha(el), - }) - }, - }) - } - - this.dots.forEach((dot, index) => { - animateAlpha(dot, index * dotAnimationOffset) - }) - } - - private destroyDots(): void { - this.dots.forEach((dot) => { - gsap.killTweensOf(dot) - dot.destroy() - }) - - this.dots = [] - } - - public destroy(): void { - this.unWatchers.forEach((unWatcher) => unWatcher()) - - this.removeChildren() - this.dimensionsObject.destroy() - this.destroyDots() - - super.destroy.call(this) - } -} diff --git a/src/pixiFunctions/nodeSprites.ts b/src/pixiFunctions/nodeSprites.ts deleted file mode 100644 index 1ebba1ee..00000000 --- a/src/pixiFunctions/nodeSprites.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { Application, Graphics, Texture } from 'pixi.js' - -export const simpleFillTextureSize = 10 - -type BoxTextures = Record<'cap' | 'body', Texture> -type BorderRectTextures = Record<'corner' | 'edge', Texture> -type RoundedBorderRectCacheKey = { - borderRadius: number, - borderColor: number, - borderWidth: number, -} - -let simpleFillTextureCache: Map | undefined -let nodeBoxTextureCache: Map | undefined -let arrowTextureCache: Map | undefined -let roundedBorderRectCache: Map | undefined - -// 0x00000000 is hexadecimal black with an unsupported alpha channel, this value simply needs to be unique for sprite registration. -const transparent = 0x00000000 -const textureSampleSettings = { - multisample: 2, - resolution: 4, -} - -export function initNodeTextureCache(): void { - simpleFillTextureCache = new Map() - nodeBoxTextureCache = new Map() - arrowTextureCache = new Map() - roundedBorderRectCache = new Map() -} - -export function destroyNodeTextureCache(): void { - if (simpleFillTextureCache) { - simpleFillTextureCache.forEach((texture) => { - texture.destroy() - }) - simpleFillTextureCache.clear() - simpleFillTextureCache = undefined - } - if (nodeBoxTextureCache) { - nodeBoxTextureCache.forEach(({ cap, body }) => { - cap.destroy() - body.destroy() - }) - nodeBoxTextureCache.clear() - nodeBoxTextureCache = undefined - } - if (arrowTextureCache) { - arrowTextureCache.forEach((texture) => { - texture.destroy() - }) - arrowTextureCache.clear() - arrowTextureCache = undefined - } - if (roundedBorderRectCache) { - roundedBorderRectCache.forEach(({ corner, edge }) => { - corner.destroy() - edge.destroy() - }) - roundedBorderRectCache.clear() - roundedBorderRectCache = undefined - } -} - -type SimpleFillTextureProps = { - pixiApp: Application, - fill: number, -} -export function getSimpleFillTexture({ - pixiApp, - fill: providedFill, -}: SimpleFillTextureProps): Texture { - const fill = !providedFill ? transparent : providedFill - - if (!simpleFillTextureCache) { - initNodeTextureCache() - } - - if (!simpleFillTextureCache?.has(fill)) { - const square = new Graphics() - square.beginFill(fill) - square.drawRect( - 0, - 0, - simpleFillTextureSize, - simpleFillTextureSize, - ) - square.endFill() - - if (fill === transparent) { - square.alpha = 0 - } - - const texture = pixiApp.renderer.generateTexture(square) - simpleFillTextureCache!.set(fill, texture) - } - - return simpleFillTextureCache!.get(fill)! -} - -type GetNodeBoxTexturesProps = { - pixiApp: Application, - fill: number, - borderRadius: number, - boxCapWidth: number, - height: number, -} -export function getNodeBoxTextures({ - pixiApp, - fill: providedFill, - borderRadius, - boxCapWidth, - height, -}: GetNodeBoxTexturesProps): BoxTextures { - const fill = !providedFill ? transparent : providedFill - - if (!nodeBoxTextureCache) { - initNodeTextureCache() - } - - if (!nodeBoxTextureCache?.has(fill)) { - const boxCap = new Graphics() - boxCap.beginFill(fill) - boxCap.moveTo(boxCapWidth, 0) - boxCap.lineTo(boxCapWidth, height) - boxCap.lineTo(0 + borderRadius, height) - boxCap.bezierCurveTo( - 0, height, - 0, height - borderRadius, - 0, height - borderRadius, - ) - boxCap.lineTo(0, borderRadius) - boxCap.bezierCurveTo( - 0, 0, - borderRadius, 0, - borderRadius, 0, - ) - boxCap.lineTo(boxCapWidth, 0) - boxCap.endFill() - - if (fill === transparent) { - boxCap.alpha = 0 - } - - const boxBody = getSimpleFillTexture({ - pixiApp, - fill, - }) - - const cap = pixiApp.renderer.generateTexture(boxCap, textureSampleSettings) - const body = boxBody - - nodeBoxTextureCache!.set(fill, { - cap, - body, - }) - } - - return nodeBoxTextureCache!.get(fill)! -} - -type GetArrowTextureProps = { - pixiApp: Application, - strokeColor: number, - edgeWidth: number, - edgeLength: number, -} -export function getArrowTexture({ - pixiApp, - strokeColor, - edgeWidth, - edgeLength, -}: GetArrowTextureProps): Texture { - if (!arrowTextureCache) { - initNodeTextureCache() - } - - if (!arrowTextureCache?.has(strokeColor)) { - const arrow = new Graphics() - arrow.lineStyle(edgeWidth, strokeColor, 1, 0.5) - arrow.moveTo(-edgeLength, -edgeLength) - arrow.lineTo(0, 0) - arrow.lineTo(-edgeLength, edgeLength) - - const arrowTexture = pixiApp.renderer.generateTexture(arrow, textureSampleSettings) - - arrowTextureCache!.set(strokeColor, arrowTexture) - } - - return arrowTextureCache!.get(strokeColor)! -} - -type GetRoundedBorderRectTexturesProps = { - pixiApp: Application, - borderRadius: number, - borderColor: number, - borderWidth: number, -} -export function getRoundedBorderRectTextures({ - pixiApp, - borderRadius, - borderColor, - borderWidth, -}: GetRoundedBorderRectTexturesProps): BorderRectTextures { - if (!roundedBorderRectCache) { - initNodeTextureCache() - } - - const cacheKey = { borderRadius, borderColor, borderWidth } - - if (!roundedBorderRectCache?.has(cacheKey)) { - const corner = new Graphics() - corner.lineStyle(borderWidth, borderColor) - corner.moveTo(0, borderRadius) - corner.bezierCurveTo( - 0, borderRadius, - 0, 0, - borderRadius, 0, - ) - - const edge = getSimpleFillTexture({ - pixiApp, - fill: borderColor, - }) - - const cornerTexture = pixiApp.renderer.generateTexture(corner, textureSampleSettings) - - roundedBorderRectCache!.set(cacheKey, { - corner: cornerTexture, - edge, - }) - } - - return roundedBorderRectCache!.get(cacheKey)! -} diff --git a/src/pixiFunctions/roundedBorderRect.ts b/src/pixiFunctions/roundedBorderRect.ts deleted file mode 100644 index dcad8fdf..00000000 --- a/src/pixiFunctions/roundedBorderRect.ts +++ /dev/null @@ -1,209 +0,0 @@ -import gsap from 'gsap' -import { Container, Sprite } from 'pixi.js' -import { GraphState } from '@/models' -import { getRoundedBorderRectTextures } from '@/pixiFunctions' - -export const roundedBorderRectAnimationDuration = 0.2 -export const roundedBorderRectAnimationEase = 'power2.out' - -type RoundedBorderRectResizeProps = { - width: number, - height: number, - animate?: boolean, -} - -type RoundedBorderRectProps = { - graphState: GraphState, - width: number, - height: number, - borderRadius: number, - borderColor: number, - borderWidth: number, -} - -export class RoundedBorderRect extends Container { - private readonly graphState - private rectWidth - private rectHeight - private readonly borderRadius - private readonly borderColor - private readonly borderWidth - - private topLeft: Sprite | undefined - private topRight: Sprite | undefined - private bottomRight: Sprite | undefined - private bottomLeft: Sprite | undefined - private topEdge: Sprite | undefined - private rightEdge: Sprite | undefined - private bottomEdge: Sprite | undefined - private leftEdge: Sprite | undefined - - public constructor({ - graphState, - width, - height, - borderRadius, - borderColor, - borderWidth, - }: RoundedBorderRectProps) { - super() - - this.graphState = graphState - this.rectWidth = width - this.rectHeight = height - - this.borderRadius = borderRadius - this.borderColor = borderColor - this.borderWidth = borderWidth - - this.initRect() - } - - private initRect(): void { - const { borderRadius, borderColor, borderWidth } = this - const { pixiApp } = this.graphState - - const { corner, edge } = getRoundedBorderRectTextures({ - pixiApp, - borderRadius, - borderColor, - borderWidth, - }) - - this.topLeft = new Sprite(corner) - - this.topRight = new Sprite(corner) - this.topRight.position.set(this.rectWidth, 0) - this.topRight.rotation = Math.PI / 2 - - this.bottomRight = new Sprite(corner) - this.bottomRight.position.set(this.rectWidth, this.rectHeight) - this.bottomRight.rotation = Math.PI - - this.bottomLeft = new Sprite(corner) - this.bottomLeft.position.set(0, this.rectHeight) - this.bottomLeft.rotation = Math.PI * 1.5 - - this.topEdge = new Sprite(edge) - this.topEdge.position.set(this.borderRadius, 0) - this.topEdge.width = this.rectWidth - this.borderRadius * 2 - this.topEdge.height = borderWidth - - this.rightEdge = new Sprite(edge) - this.rightEdge.position.set(this.rectWidth - this.borderWidth, this.borderRadius) - this.rightEdge.height = this.rectHeight - this.borderRadius * 2 - this.rightEdge.width = borderWidth - - this.bottomEdge = new Sprite(edge) - this.bottomEdge.position.set(this.borderRadius, this.rectHeight - this.borderWidth) - this.bottomEdge.width = this.rectWidth - this.borderRadius * 2 - this.bottomEdge.height = borderWidth - - this.leftEdge = new Sprite(edge) - this.leftEdge.position.set(0, this.borderRadius) - this.leftEdge.height = this.rectHeight - this.borderRadius * 2 - this.leftEdge.width = borderWidth - - this.addChild(this.topLeft) - this.addChild(this.topRight) - this.addChild(this.bottomRight) - this.addChild(this.bottomLeft) - - this.addChild(this.topEdge) - this.addChild(this.rightEdge) - this.addChild(this.bottomEdge) - this.addChild(this.leftEdge) - - this.resize({ width: this.rectWidth, height: this.rectHeight }) - } - - public resize({ - width, - height, - animate, - }: RoundedBorderRectResizeProps): void { - const { - topRight, - bottomRight, - bottomLeft, - topEdge, - rightEdge, - bottomEdge, - leftEdge, - } = this - this.scale.x = 1 - this.rectWidth = width - this.rectHeight = height - - const minRadiusWidth = this.borderRadius * 2 - const isWidthTooSmall = width < minRadiusWidth - - const adaptedWidth = isWidthTooSmall ? minRadiusWidth : width - - if (width < minRadiusWidth) { - this.scale.x = width / minRadiusWidth - } - - if (!animate || this.graphState.suppressMotion.value) { - topRight!.position.x = adaptedWidth - bottomRight!.position.set(adaptedWidth, this.rectHeight) - bottomLeft!.position.y = this.rectHeight - - topEdge!.width = adaptedWidth - minRadiusWidth - rightEdge!.height = this.rectHeight - minRadiusWidth - rightEdge!.position.x = adaptedWidth - this.borderWidth - bottomEdge!.width = adaptedWidth - minRadiusWidth - bottomEdge!.position.y = this.rectHeight - this.borderWidth - leftEdge!.height = this.rectHeight - minRadiusWidth - return - } - - const animationOptions = { - duration: roundedBorderRectAnimationDuration, - ease: roundedBorderRectAnimationEase, - } - - gsap.to(topRight!, { x: adaptedWidth, ...animationOptions }) - gsap.to(bottomRight!, { x: adaptedWidth, y: this.rectHeight, ...animationOptions }) - gsap.to(bottomLeft!, { y: this.rectHeight, ...animationOptions }) - - gsap.to(topEdge!, { width: adaptedWidth - minRadiusWidth, ...animationOptions }) - gsap.to(rightEdge!, { - height: this.rectHeight - minRadiusWidth, - x: adaptedWidth - this.borderWidth, - ...animationOptions, - }) - gsap.to(bottomEdge!, { - width: adaptedWidth - minRadiusWidth, - y: this.rectHeight - this.borderWidth, - ...animationOptions, - }) - gsap.to(leftEdge!, { height: this.rectHeight - minRadiusWidth, ...animationOptions }) - } - - private killTweens(): void { - const { - topRight, - bottomRight, - bottomLeft, - topEdge, - rightEdge, - bottomEdge, - leftEdge, - } = this - gsap.killTweensOf([ - topRight, - bottomRight, - bottomLeft, - topEdge, - rightEdge, - bottomEdge, - leftEdge, - ]) - } - - public destroy(): void { - this.killTweens() - super.destroy.call(this) - } -} \ No newline at end of file diff --git a/src/pixiFunctions/subNodesToggle.ts b/src/pixiFunctions/subNodesToggle.ts deleted file mode 100644 index c1aa1269..00000000 --- a/src/pixiFunctions/subNodesToggle.ts +++ /dev/null @@ -1,327 +0,0 @@ -import gsap from 'gsap' -import { Container, Sprite } from 'pixi.js' -import { watch, WatchStopHandle } from 'vue' -import { GraphState } from '@/models' -import { - getArrowTexture, - getNodeBoxTextures, - getSimpleFillTexture, - RoundedBorderRect -} from '@/pixiFunctions' -import { TimelineItem } from '@/types/timeline' -import { colorToHex } from '@/utilities' - -const hoverShadePieces = { - leftCap: 'leftCap', - body: 'body', - rightCap: 'rightCap', -} - -const toggleAnimationDuration = 0.2 -// At which scale does the toggle become too hard to see and doesn't need to be drawn. -const toggleScaleCullingThreshold = 0.2 - -type SubNodesToggleProps = { - graphState: GraphState, - nodeData: TimelineItem, - size: number, - floating?: boolean, -} - -export class SubNodesToggle extends Container { - private readonly graphState - private readonly nodeData - private readonly size - - private isFloating - private isExpanded = false - private textColor: number - - private readonly toggleBox = new Container() - private readonly hoverShade = new Container() - private toggleArrow: Sprite | undefined - private toggleBorder: RoundedBorderRect | undefined - private divider: Sprite | undefined - - private readonly unWatchers: WatchStopHandle[] = [] - - public constructor({ - graphState, - nodeData, - size, - floating, - }: SubNodesToggleProps) { - super() - - this.interactive = true - this.buttonMode = true - graphState.cull.add(this) - - this.graphState = graphState - this.nodeData = nodeData - this.size = size - this.isFloating = floating - - this.textColor = this.getTextColor() - - this.addChild(this.toggleBox) - this.addChild(this.hoverShade) - this.initShapes() - - this.on('pointerover', () => { - this.hover() - }) - this.on('pointerout', () => { - this.unHover() - }) - - this.initWatchers() - } - - private initWatchers(): void { - const { viewport, styleOptions, styleNode } = this.graphState - - this.unWatchers.push( - watch([styleOptions, styleNode], () => { - this.redraw() - }, { deep: true }), - ) - - viewport.on('frame-end', () => { - if (viewport.scale.x < toggleScaleCullingThreshold) { - this.visible = false - } else { - this.visible = true - } - }) - } - - private initShapes(): void { - this.drawToggleBox() - this.drawHoverShade() - this.drawDivider() - this.drawToggleArrow() - this.drawToggleBorder() - } - - private drawToggleBox(): void { - this.toggleBox.removeChildren() - - if (!this.isFloating) { - return - } - - const { size, toggleBox } = this - const { pixiApp, styleOptions } = this.graphState - - const { colorButtonBg, borderRadiusButton } = styleOptions.value - - const { cap, body } = getNodeBoxTextures({ - pixiApp, - fill: colorButtonBg, - borderRadius: borderRadiusButton, - boxCapWidth: borderRadiusButton, - height: size, - }) - - const bgLeftCap = new Sprite(cap) - - const bgBody = new Sprite(body) - bgBody.x = borderRadiusButton - bgBody.width = size - borderRadiusButton * 2 - bgBody.height = size - - const bgRightCap = new Sprite(cap) - bgRightCap.scale.x = -1 - bgRightCap.x = size - - toggleBox.addChild(bgLeftCap) - toggleBox.addChild(bgBody) - toggleBox.addChild(bgRightCap) - } - - private drawHoverShade(): void { - const { - hoverShade, - size, - isFloating, - } = this - const { - borderRadiusButton, - colorButtonBgHover, - } = this.graphState.styleOptions.value - const { - onFillSubNodeToggleHoverBg, - onFillSubNodeToggleHoverBgAlpha, - } = this.graphState.styleNode.value(this.nodeData) - const nonFloatingHoverBg = colorToHex(onFillSubNodeToggleHoverBg) - - hoverShade.removeChildren() - - const { cap, body } = getNodeBoxTextures({ - pixiApp: this.graphState.pixiApp, - fill: isFloating ? colorButtonBgHover : nonFloatingHoverBg, - borderRadius: borderRadiusButton, - boxCapWidth: borderRadiusButton, - height: size, - }) - - const leftCap = new Sprite(cap) - leftCap.name = hoverShadePieces.leftCap - leftCap.alpha = isFloating ? 1 : onFillSubNodeToggleHoverBgAlpha - - const bodySprite = new Sprite(body) - bodySprite.name = hoverShadePieces.body - bodySprite.x = borderRadiusButton - bodySprite.width = isFloating - ? size - borderRadiusButton * 2 - : size - borderRadiusButton - bodySprite.height = size - bodySprite.alpha = isFloating ? 1 : onFillSubNodeToggleHoverBgAlpha - - const rightCap = new Sprite(cap) - rightCap.name = hoverShadePieces.rightCap - rightCap.scale.x = -1 - rightCap.x = size - rightCap.alpha = isFloating ? 1 : 0 - - hoverShade.addChild(leftCap) - hoverShade.addChild(bodySprite) - hoverShade.addChild(rightCap) - - hoverShade.alpha = 0 - } - - private drawDivider(): void { - this.divider?.destroy() - - if (this.isFloating) { - return - } - - const { size, textColor } = this - const { pixiApp } = this.graphState - - const fillTexture = getSimpleFillTexture({ - pixiApp, - fill: textColor, - }) - - this.divider = new Sprite(fillTexture) - this.divider.width = 1 - this.divider.height = size - this.divider.x = size - - this.addChild(this.divider) - } - - private drawToggleArrow(): void { - const { size, textColor, isExpanded } = this - const { pixiApp } = this.graphState - - this.toggleArrow?.destroy() - - const arrowTexture = getArrowTexture({ - pixiApp, - strokeColor: textColor, - edgeWidth: 2, - edgeLength: 8, - }) - - this.toggleArrow = new Sprite(arrowTexture) - this.toggleArrow.transform.rotation = isExpanded ? Math.PI / 2 * -1 : Math.PI / 2 - this.toggleArrow.anchor.set(0.5, 0.5) - this.toggleArrow.position.set(size / 2, size / 2) - - this.addChild(this.toggleArrow) - } - - private drawToggleBorder(): void { - this.toggleBorder?.destroy() - - const { size, isFloating } = this - const { - colorButtonBorder, - borderRadiusButton, - spacingButtonBorderWidth, - } = this.graphState.styleOptions.value - - if (!colorButtonBorder || !isFloating) { - return - } - - this.toggleBorder = new RoundedBorderRect({ - graphState: this.graphState, - width: size, - height: size, - borderRadius: borderRadiusButton, - borderWidth: spacingButtonBorderWidth, - borderColor: colorButtonBorder, - }) - - this.addChild(this.toggleBorder) - } - - private setToggleArrowRotation(rotation: number): void { - const { suppressMotion } = this.graphState - - if (!this.toggleArrow) { - return - } - - gsap.to(this.toggleArrow, { - rotation, - duration: suppressMotion.value ? 0 : toggleAnimationDuration, - ease: 'power2.inOut', - }) - } - - private getTextColor(): number { - const { styleOptions, styleNode } = this.graphState - const { colorTextDefault, colorTextInverse } = styleOptions.value - const { inverseTextOnFill } = styleNode.value(this.nodeData) - - if (this.isFloating) { - return colorTextDefault - } - - return inverseTextOnFill ? colorTextInverse : colorTextDefault - } - - private redraw(): void { - this.textColor = this.getTextColor() - this.initShapes() - } - - private hover(): void { - this.hoverShade.alpha = 1 - } - - private unHover(): void { - this.hoverShade.alpha = 0 - } - - public setExpanded(): void { - this.isExpanded = true - this.setToggleArrowRotation(Math.PI / 2 * -1) - } - - public setCollapsed(): void { - this.isExpanded = false - this.setToggleArrowRotation(Math.PI / 2) - } - - public updateFloatingState(value: boolean): void { - if (this.isFloating === value) { - return - } - - this.isFloating = value - this.redraw() - } - - public destroy(): void { - this.graphState.cull.remove(this) - super.destroy.call(this) - } -} diff --git a/src/pixiFunctions/timeScale.ts b/src/pixiFunctions/timeScale.ts deleted file mode 100644 index 13a90811..00000000 --- a/src/pixiFunctions/timeScale.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - DateToX, - XToDate, - TimeScale, - TimeScaleArgs -} from '@/models' - -export const createTimeScale = ({ - minimumStartTime, - graphXDomain, - initialOverallTimeSpan, -}: TimeScaleArgs): TimeScale => { - return { - dateToX: createDateToXScale( - minimumStartTime, - graphXDomain, - initialOverallTimeSpan, - ), - xToDate: createXToDateScale( - minimumStartTime, - graphXDomain, - initialOverallTimeSpan, - ), - } -} - -function createDateToXScale(minStartTime: number, overallWidth: number, overallTimeSpan: number): DateToX { - return function(date: Date): number { - return Math.ceil((date.getTime() - minStartTime) * (overallWidth / overallTimeSpan)) - } -} - -function createXToDateScale(minStartTime: number, overallWidth: number, overallTimeSpan: number): XToDate { - return function(xPosition: number): Date { - return new Date(Math.ceil(minStartTime + xPosition * (overallTimeSpan / overallWidth))) - } -} diff --git a/src/pixiFunctions/timelineEdge.ts b/src/pixiFunctions/timelineEdge.ts deleted file mode 100644 index 21300150..00000000 --- a/src/pixiFunctions/timelineEdge.ts +++ /dev/null @@ -1,255 +0,0 @@ -import { Container, Point, SimpleRope, Sprite, Texture } from 'pixi.js' -import { watch, WatchStopHandle } from 'vue' -import { GraphState } from '@/models' -import { - TimelineNode, - timelineNodeBoxName, - getArrowTexture, - getSimpleFillTexture, - simpleFillTextureSize -} from '@/pixiFunctions' - -const minimumBezier = 64 -const edgeFidelity = 20 - -type TimelineEdgeProps = { - sourceNode: TimelineNode, - targetNode: TimelineNode, - state: GraphState, -} - -export class TimelineEdge extends Container { - private readonly sourceNode - private readonly targetNode - private readonly graphState - - private sourceX: number = 0 - private sourceY: number = 0 - private targetX: number = 0 - private targetY: number = 0 - private sourceControlPointX: number = 0 - private sourceControlPointY: number = 0 - private targetControlPointX: number = 0 - private targetControlPointY: number = 0 - - private readonly edgePoints: Point[] - private readonly edge: SimpleRope - private readonly arrow = new Container() - - private readonly unWatchers: WatchStopHandle[] = [] - - public constructor({ - sourceNode, - targetNode, - state, - }: TimelineEdgeProps) { - super() - - this.sourceNode = sourceNode - this.targetNode = targetNode - this.graphState = state - - this.assignBezierPositions() - - this.edgePoints = this.initEdgePoints() - this.edge = this.initEdge() - this.addChild(this.edge) - - this.drawArrow() - this.addChild(this.arrow) - - this.initCulling() - this.initWatchers() - } - - private initCulling(): void { - // Note: edges are not culled because problems arise when expanding/contracting subNodes. - const { cull } = this.graphState - - cull.add(this.arrow) - } - - private initWatchers(): void { - const { styleOptions, viewport } = this.graphState - - this.unWatchers.push( - watch([styleOptions], () => { - this.updateStyle() - }, { deep: true }), - ) - - viewport.on('frame-end', () => { - if (this.sourceNode.destroyed || this.targetNode.destroyed) { - return - } - - if (this.hasEdgeChanged()) { - this.update() - } - }) - } - - private assignBezierPositions(): void { - this.sourceX = this.getSourceX() - this.sourceY = this.getNodeY(this.sourceNode) - this.targetX = this.targetNode.x - this.targetY = this.getNodeY(this.targetNode) - - this.sourceControlPointX = this.getXBezier(this.sourceX) - this.sourceControlPointY = this.sourceY - this.targetControlPointX = this.getXBezier(this.targetX, true) - this.targetControlPointY = this.targetY - } - - private initEdge(): SimpleRope { - const { spacingEdgeWidth } = this.graphState.styleOptions.value - const texture = this.getEdgeTexture() - - return new SimpleRope(texture, this.edgePoints, spacingEdgeWidth / simpleFillTextureSize) - } - - private initEdgePoints(): Point[] { - const points: Point[] = [] - - for (let i = 0; i < edgeFidelity; i++) { - const point = i === edgeFidelity - 1 - ? { x: this.targetX, y: this.targetY } - : this.getPointBezierPosition(i / edgeFidelity) - points.push(new Point(point.x, point.y)) - } - - // the target anchor point accommodates for bezier curves often end short of the target - const targetAnchorPointPosition = this.getTargetAnchorPointPosition() - const targetAnchorPoint = new Point(targetAnchorPointPosition.x, targetAnchorPointPosition.y) - points.push(targetAnchorPoint) - - return points - } - - private drawArrow(): void { - const { arrow, targetX, targetY } = this - const { pixiApp, styleOptions } = this.graphState - const { colorEdge, spacingEdgeWidth, spacingNodeEdgeLength } = styleOptions.value - - const arrowTexture = getArrowTexture({ - pixiApp, - strokeColor: colorEdge, - edgeWidth: spacingEdgeWidth, - edgeLength: spacingNodeEdgeLength, - }) - - const arrowSprite = new Sprite(arrowTexture) - arrowSprite.anchor.set(1, 0.5) - - arrow.addChild(arrowSprite) - arrow.position.set(targetX, targetY) - } - - /** - * Update functions - */ - public update(): void { - this.assignBezierPositions() - this.arrow.position.set(this.targetX, this.targetY) - - this.edgePoints.forEach((point, index) => { - const newPoint = index === this.edgePoints.length - 1 - ? this.getTargetAnchorPointPosition() - : this.getPointBezierPosition(index / edgeFidelity) - point.set(newPoint.x, newPoint.y) - }) - } - - private updateStyle(): void { - this.arrow.removeChildren() - this.drawArrow() - - this.edge.texture = this.getEdgeTexture() - } - - /** - * Utilities - */ - private hasEdgeChanged(): boolean { - const currentSourceX = this.getSourceX() - const currentSourceY = this.getNodeY(this.sourceNode) - const currentTargetX = this.targetNode.x - const currentTargetY = this.getNodeY(this.targetNode) - - return currentSourceX !== this.sourceX - || currentSourceY !== this.sourceY - || currentTargetX !== this.targetX - || currentTargetY !== this.targetY - } - - private getSourceX(): number { - return this.sourceNode.x + this.sourceNode.getChildByName(timelineNodeBoxName).getLocalBounds().width - } - - private getNodeY(node: TimelineNode): number { - return node.y + node.getChildByName(timelineNodeBoxName).getLocalBounds().height / 2 - } - - private readonly getXBezier = (xPos: number, upstream?: boolean): number => { - const { sourceX, targetX } = this - - const bezierLength = (targetX - sourceX) / 2 - return xPos + (bezierLength > minimumBezier ? bezierLength : minimumBezier) * (upstream ? -1 : 1) - } - - private getEdgeTexture(): Texture { - const { pixiApp, styleOptions } = this.graphState - const { colorEdge } = styleOptions.value - - const texture = getSimpleFillTexture({ - pixiApp, - fill: colorEdge, - }) - - return texture - } - - private readonly getPointBezierPosition = (pointOnPath: number): { x: number, y: number } => { - // https://javascript.info/bezier-curve#de-casteljau-s-algorithm - const { - sourceX, - sourceY, - targetX, - targetY, - sourceControlPointX, - sourceControlPointY, - targetControlPointX, - targetControlPointY, - } = this - - const cx1 = sourceX + (sourceControlPointX - sourceX) * pointOnPath - const cy1 = sourceY + (sourceControlPointY - sourceY) * pointOnPath - const cx2 = sourceControlPointX + (targetControlPointX - sourceControlPointX) * pointOnPath - const cy2 = sourceControlPointY + (targetControlPointY - sourceControlPointY) * pointOnPath - const cx3 = targetControlPointX + (targetX - targetControlPointX) * pointOnPath - const cy3 = targetControlPointY + (targetY - targetControlPointY) * pointOnPath - - const cx4 = cx1 + (cx2 - cx1) * pointOnPath - const cy4 = cy1 + (cy2 - cy1) * pointOnPath - const cx5 = cx2 + (cx3 - cx2) * pointOnPath - const cy5 = cy2 + (cy3 - cy2) * pointOnPath - - const x = cx4 + (cx5 - cx4) * pointOnPath - const y = cy4 + (cy5 - cy4) * pointOnPath - - return { x, y } - } - - private getTargetAnchorPointPosition(): { x: number, y: number } { - const xOffset = 2 - - return { x: this.targetX - xOffset, y: this.targetY } - } - - public destroy(): void { - const { cull } = this.graphState - cull.remove(this.arrow) - this.unWatchers.forEach((unwatch) => unwatch()) - super.destroy.call(this) - } -} diff --git a/src/pixiFunctions/timelineNode.ts b/src/pixiFunctions/timelineNode.ts deleted file mode 100644 index 90874342..00000000 --- a/src/pixiFunctions/timelineNode.ts +++ /dev/null @@ -1,1027 +0,0 @@ -import gsap from 'gsap' -import { - BitmapText, - Container, - Sprite, - TextMetrics, - UPDATE_PRIORITY -} from 'pixi.js' -import { Ref, unref, watch, WatchStopHandle } from 'vue' -import { - GraphState, - NodeLayoutRow, - NodesLayout, - NodeSelectionEvent -} from '@/models' -import { - getBitmapFonts, - getNodeBoxTextures, - TimelineNodes, - RoundedBorderRect, - SubNodesToggle, - timelineUpdateEvent, - roundedBorderRectAnimationDuration, - roundedBorderRectAnimationEase, - LoadingIndicator -} from '@/pixiFunctions' -import { TimelineData, TimelineItem } from '@/types/timeline' -import { colorToHex } from '@/utilities/style' - -export const nodeClickEvents = { - nodeDetails: 'nodeDetailsClick', - subNodesToggle: 'subNodesToggleClick', -} -export const nodeResizeEvent = 'nodeResize' -export const nodeAnimationDurations = { - fadeIn: 0.25, - move: 0.5, -} -// The name giving to the box container, this is used by -// edges to determine their positions -export const timelineNodeBoxName = 'box' - -const noSubNodesMessageText = 'None' -// At which scale do node details become too hard to see and don't need to be drawn. -const nodeElementScaleCullingThreshold = 0.2 - -type TimelineNodeUpdatePositionProps = { - skipAnimation?: boolean, - includeXPos?: boolean, -} - -type TimelineNodeProps = { - nodeData: TimelineItem, - state: GraphState, - layout: Ref, - layoutRows: Ref, -} - -export class TimelineNode extends Container { - public nodeData - private readonly state - private readonly layout - private readonly layoutRows - - private currentState: string - private readonly hasSubNodes: boolean = false - private isRunningNode: boolean = false - private runningNodeTicker: (() => void) | null = null - private readonly unWatchers: WatchStopHandle[] = [] - - // the position must be initialized before it will update itself. - public positionInitialized = false - private nodeWidth - private readonly nodeHeight - - private readonly boxCapWidth: number = 0 - private readonly box = new Container() - private leftBoxCap: Sprite | undefined - private rightBoxCap: Sprite | undefined - private boxBody: Sprite | undefined - - private subNodesToggle: SubNodesToggle | undefined - private subNodesToggleWidth = 0 - private isSubNodesToggleFloating: boolean = false - private isLoadingSubNodes: boolean = false - private subNodesLoadingIndicator: LoadingIndicator | null = null - private noSubNodesMessage: BitmapText | null = null - - private label: BitmapText | undefined - private apxLabelWidth = 0 - private isLabelInBox = true - - private isSubNodesExpanded = false - private readonly subNodesOutlineContainer = new Container() - private subNodesOutline: RoundedBorderRect | undefined - private subNodesContent: TimelineNodes | null = null - private subNodesHeight = 0 - private subNodesContentTicker: (() => void) | null = null - - private isSelected = false - private selectedRing: RoundedBorderRect | undefined - - public constructor({ - nodeData, - state, - layout, - layoutRows, - }: TimelineNodeProps) { - super() - - this.interactive = false - - this.nodeData = nodeData - this.state = state - this.layout = layout - this.layoutRows = layoutRows - - this.currentState = nodeData.state.toString() - this.hasSubNodes = nodeData.subflowRunId !== null - this.updateIsRunningNode() - - this.boxCapWidth = state.styleOptions.value.borderRadiusNode - this.nodeWidth = this.getNodeWidth() - this.nodeHeight = this.getNodeHeight() - - this.addChild(this.subNodesOutlineContainer) - this.drawSubNodesOutline() - - this.initBox() - this.initSubNodesToggle() - this.drawLabel() - this.initSelectedRing() - - this.initWatchers() - } - - private initWatchers(): void { - const { - layoutRows, - unWatchers, - hasSubNodes, - isRunningNode, - } = this - const { - pixiApp, - styleOptions, - styleNode, - selectedNodeId, - subNodeLabels, - expandedSubNodes, - viewport, - } = this.state - - unWatchers.push( - watch(layoutRows, () => { - this.updatePosition() - }), - watch([styleOptions, styleNode], () => { - this.drawBox() - this.drawLabel() - - if (this.noSubNodesMessage) { - this.drawNoSubNodesMessage() - } - }, { deep: true }), - watch(selectedNodeId, selected => { - const nodeSelectionId = this.nodeData.subflowRunId ?? this.nodeData.id - const isCurrentSelection = selected && selected === nodeSelectionId - - if (isCurrentSelection) { - this.select() - return - } - if (this.isSelected) { - this.deselect() - } - }), - ) - - if (hasSubNodes) { - unWatchers.push( - watch(expandedSubNodes, () => { - if (!this.nodeData.subflowRunId) { - return - } - - if (!this.isSubNodesExpanded && expandedSubNodes.value.has(this.nodeData.subflowRunId)) { - this.isSubNodesExpanded = true - this.subNodesToggle?.setExpanded() - - const subNodesData = this.getSubNodesData() - - if (subNodesData.size === 0 && !this.isLoadingSubNodes) { - this.isLoadingSubNodes = true - this.drawLoadingSubNodes() - return - } - - this.expandSubNodes() - return - } - - if (this.isSubNodesExpanded) { - if (!expandedSubNodes.value.has(this.nodeData.subflowRunId)) { - this.subNodesToggle?.setCollapsed() - - this.isSubNodesExpanded = false - this.isLoadingSubNodes = false - - this.collapseSubNodes() - return - } - - const newData = this.getSubNodesData() - - if (this.isLoadingSubNodes) { - this.isLoadingSubNodes = false - this.destroySubNodesLoadingIndicator() - if (newData.size === 0) { - this.drawNoSubNodesMessage() - return - } - } - - if (!this.subNodesContent) { - this.expandSubNodes() - return - } - - this.subNodesContent.update(newData) - } - }, { deep: true }), - watch(subNodeLabels, () => { - if (this.getLabelText() !== this.label?.text) { - this.drawLabel(true) - } - }, { deep: true }), - ) - } - - if (isRunningNode) { - this.runningNodeTicker = () => { - this.update() - } - - pixiApp.ticker.add(this.runningNodeTicker) - } - - viewport.on('frame-end', () => { - if (viewport.scale.x < nodeElementScaleCullingThreshold) { - if (this.label) { - this.label.visible = false - } - if (this.subNodesOutline && !this.isSubNodesExpanded) { - this.subNodesOutline.visible = false - } - return - } - if (this.label) { - this.label.visible = true - } - if (this.subNodesOutline) { - this.subNodesOutline.visible = true - } - }) - } - - private drawSubNodesOutline(): void { - if (!this.hasSubNodes) { - return - } - - this.subNodesOutline?.destroy() - - const { nodeHeight, state: graphState } = this - const { styleOptions, styleNode } = graphState - const { - borderRadiusNode, - alphaSubNodesOutlineDimmed, - spacingSubNodesOutlineBorderWidth, - spacingSubNodesOutlineOffset, - } = styleOptions.value - const { fill } = styleNode.value(this.nodeData) - const outlineColor = colorToHex(fill) - - const width = this.getOutlineWidth() - - this.subNodesOutline = new RoundedBorderRect({ - graphState, - width, - height: nodeHeight, - borderRadius: borderRadiusNode, - borderWidth: spacingSubNodesOutlineBorderWidth, - borderColor: outlineColor, - }) - this.subNodesOutline.position.set( - spacingSubNodesOutlineOffset, - spacingSubNodesOutlineOffset, - ) - this.subNodesOutline.alpha = alphaSubNodesOutlineDimmed - - this.subNodesOutlineContainer.addChild(this.subNodesOutline) - } - - private initBox(): void { - const { box } = this - box.name = timelineNodeBoxName - this.drawBox() - this.addChild(box) - - box.interactive = true - box.buttonMode = true - box.on('click', () => { - this.emitSelection() - }) - this.state.cull.add(box) - } - - private drawBox(): void { - const { pixiApp, styleOptions, styleNode } = this.state - const { isRunningNode, nodeWidth, nodeHeight, box, boxCapWidth } = this - const { borderRadiusNode } = styleOptions.value - const { fill } = styleNode.value(this.nodeData) - const hexadecimalFill = colorToHex(fill) - - this.leftBoxCap?.destroy() - this.boxBody?.destroy() - this.rightBoxCap?.destroy() - this.box.removeChildren() - - const { cap, body } = getNodeBoxTextures({ - pixiApp, - fill: hexadecimalFill, - borderRadius: borderRadiusNode, - boxCapWidth, - height: nodeHeight, - }) - - this.leftBoxCap = new Sprite(cap) - - this.boxBody = new Sprite(body) - this.boxBody.width = this.getBoxBodyWidth() - this.boxBody.height = nodeHeight - this.boxBody.position.set(boxCapWidth, 0) - - box.addChild(this.leftBoxCap) - box.addChild(this.boxBody) - - if (!isRunningNode) { - this.rightBoxCap = new Sprite(cap) - this.rightBoxCap.scale.x = -1 - this.rightBoxCap.position.x = nodeWidth - - box.addChild(this.rightBoxCap) - } - } - - private initSubNodesToggle(): void { - if (!this.hasSubNodes) { - return - } - - const { state: graphState, nodeWidth, nodeHeight, nodeData } = this - const { spacingNodeLabelMargin, borderRadiusNode } = this.state.styleOptions.value - - this.subNodesToggleWidth = nodeHeight - this.isSubNodesToggleFloating = this.subNodesToggleWidth + borderRadiusNode > nodeWidth - - this.subNodesToggle = new SubNodesToggle({ - graphState, - nodeData, - floating: this.isSubNodesToggleFloating, - size: this.subNodesToggleWidth, - }) - this.subNodesToggle.position.x = this.isSubNodesToggleFloating - ? nodeWidth + spacingNodeLabelMargin - : 0 - - this.subNodesToggle.on('click', () => { - this.emitSubNodesToggle() - }) - - this.addChild(this.subNodesToggle) - } - - private async drawLabel(newLabelText?: boolean): Promise { - const { apxLabelWidth, nodeData } = this - const { styleOptions, styleNode, cull } = this.state - - const textStyles = await getBitmapFonts(styleOptions.value) - const { spacingNodeXPadding } = styleOptions.value - const { inverseTextOnFill } = styleNode.value(nodeData) - const labelStyleOnFill = inverseTextOnFill ? textStyles.nodeTextInverse : textStyles.nodeTextDefault - - const labelText = this.getLabelText() - - if (apxLabelWidth === 0 || newLabelText) { - // the text metrics are consistently a bit off, so we add a buffer percentage - const labelWidthBufferPercentage = 7 - const apxLabelTextWidth = - TextMetrics.measureText(labelText, textStyles.nodeTextStyles).width - * (1 + labelWidthBufferPercentage / 100) - - this.apxLabelWidth = apxLabelTextWidth + spacingNodeXPadding * 2 - } - - this.isLabelInBox = this.checkIsLabelInBox() - - const labelStyle = this.isLabelInBox ? labelStyleOnFill : textStyles.nodeTextDefault - - if (this.label) { - cull.remove(this.label) - this.label.destroy() - } - - this.label = new BitmapText(labelText, labelStyle) - this.updateLabelPosition() - - if (!this.isLabelInBox) { - this.label.interactive = true - this.label.buttonMode = true - this.label.on('click', () => { - this.emitSelection() - }) - } - - cull.add(this.label) - this.addChild(this.label) - } - - private initSelectedRing(): void { - const { width, height, margin } = this.getSelectedRingSize() - const { - colorNodeSelection, - spacingNodeSelectionWidth, - borderRadiusNode, - } = this.state.styleOptions.value - - this.selectedRing = new RoundedBorderRect({ - graphState: this.state, - width, - height, - borderRadius: borderRadiusNode, - borderColor: colorNodeSelection, - borderWidth: spacingNodeSelectionWidth, - }) - this.selectedRing.position.set(-margin, -margin) - this.selectedRing.alpha = 0 - - this.addChild(this.selectedRing) - } - - /** - * Node Selection - */ - public select(): void { - this.isSelected = true - this.selectedRing!.alpha = 1 - - this.centerViewportToNodeAfterDelay() - } - - public deselect(): void { - this.isSelected = false - this.selectedRing!.alpha = 0 - - if (!this.state.selectedNodeId.value) { - this.centerViewportToNodeAfterDelay() - } - } - - private centerViewportToNodeAfterDelay(): void { - const { viewport, suppressMotion } = this.state - setTimeout(() => { - const xPos = (this.worldTransform.tx - viewport.x) / viewport.scale.x + this.box.width / 2 - const yPos = (this.worldTransform.ty - viewport.y) / viewport.scale.y + this.box.height / 2 - - viewport.animate({ - position: { - x: xPos, - y: yPos, - }, - time: suppressMotion.value ? 0 : 1000, - ease: 'easeInOutQuad', - removeOnInterrupt: true, - }) - }, 100) - } - - /** - * Subnodes - */ - private expandSubNodes(): void { - if (!this.nodeData.subflowRunId) { - return - } - - const subNodeContent = this.state.expandedSubNodes.value.get(this.nodeData.subflowRunId) - - if (!this.hasSubNodes || !subNodeContent) { - return - } - - const subNodesData = unref(subNodeContent.data) - - this.subNodesContent?.destroy() - - this.subNodesContent = new TimelineNodes({ - isSubNodes: true, - data: subNodesData, - state: this.state, - }) - - this.subNodesContent.on(nodeClickEvents.nodeDetails, (nodeSelectionValue) => { - this.emit(nodeClickEvents.nodeDetails, nodeSelectionValue) - }) - this.subNodesContent.on(nodeClickEvents.subNodesToggle, (id) => { - this.emitSubNodesToggle(id) - }) - - this.updateSubNodesContentPosition() - this.subNodesContent.on(timelineUpdateEvent, () => this.updateSubNodesContentPosition()) - - this.addChild(this.subNodesContent) - - this.initSubNodesTicker() - } - - private drawLoadingSubNodes(): void { - const { state: graphState, box } = this - const { spacingNodeMargin } = graphState.styleOptions.value - - this.destroySubNodesLoadingIndicator() - - this.subNodesLoadingIndicator = new LoadingIndicator({ graphState }) - this.subNodesLoadingIndicator.position.set( - box.width / 2 - this.subNodesLoadingIndicator.width / 2, - box.y + box.height + spacingNodeMargin, - ) - - this.addChild(this.subNodesLoadingIndicator) - - this.initSubNodesTicker() - } - - private async drawNoSubNodesMessage(): Promise { - const { box } = this - const { styleOptions } = this.state - const textStyles = await getBitmapFonts(styleOptions.value) - - this.destroyNoSubNodesMessage() - - this.noSubNodesMessage = new BitmapText(noSubNodesMessageText, textStyles.nodeTextSubdued) - this.noSubNodesMessage.anchor.set(0.5, 0) - this.noSubNodesMessage.position.set( - box.width / 2, - box.y + box.height + styleOptions.value.spacingNodeMargin, - ) - - this.addChild(this.noSubNodesMessage) - - this.initSubNodesTicker() - } - - private updateSubNodesContentPosition(): void { - const { subNodesContent, box } = this - const { spacingNodeMargin } = this.state.styleOptions.value - - if (!subNodesContent) { - return - } - - // The subNodes nodes are positioned relative to the global timeline, but we're drawing - // the subNodes container relative to this node, so we need to offset the X to compensate. - const earliestSubNodes = subNodesContent.getEarliestNodeStart() - const xPosNegativeOffset = earliestSubNodes ? -this.state.timeScale.dateToX(earliestSubNodes) : 0 - - const yPos = box.y + box.height + spacingNodeMargin - - subNodesContent.position.set( - xPosNegativeOffset, - yPos, - ) - } - - private initSubNodesTicker(): void { - const { pixiApp } = this.state - - if (this.subNodesContentTicker) { - return - } - - this.subNodesHeight = 0 - - this.subNodesContentTicker = () => { - const newSubNodesHeight = this.getSubContentHeight() - - if (newSubNodesHeight !== this.subNodesHeight) { - this.updateSubNodesOutlineSize() - this.updateSelectedRingSize() - this.subNodesHeight = newSubNodesHeight - this.emit(nodeResizeEvent) - } - } - pixiApp.ticker.add( - this.subNodesContentTicker, - null, UPDATE_PRIORITY.LOW, - ) - } - - private async collapseSubNodes(): Promise { - this.destroySubNodesContent() - - this.updateSelectedRingSize() - await this.updateSubNodesOutlineSize() - - this.emit(nodeResizeEvent) - } - - /** - * Update Functions - */ - public update(newData?: TimelineItem): void { - let hasNewState = false - let hasNewLabelText = false - - if (newData) { - this.nodeData = newData - - hasNewState = this.currentState !== this.nodeData.state - this.currentState = this.nodeData.state.toString() - - this.updateIsRunningNode() - - hasNewLabelText = this.label?.text !== this.nodeData.label - } - - if (this.isRunningNode && this.nodeData.end) { - this.isRunningNode = false - this.destroyRunningNodeTicker() - } - - const nodeWidth = this.getNodeWidth() - - if (hasNewState) { - this.drawBox() - this.drawSubNodesOutline() - } - - if (hasNewLabelText) { - this.drawLabel() - } - - if (nodeWidth !== this.nodeWidth) { - this.nodeWidth = nodeWidth - - this.updateBoxWidth() - this.updateSelectedRingSize() - this.updateSubNodesOutlineSize(true) - this.updateSubNodesTogglePosition() - - // 2px tolerance avoids the label bouncing in/out of the box - const isLabelInBoxChanged = this.isLabelInBox !== this.checkIsLabelInBox(2) - - if (!hasNewLabelText && isLabelInBoxChanged) { - this.drawLabel() - } else if (!this.isLabelInBox) { - this.updateLabelPosition() - } - } - } - - private updatePosition(options?: TimelineNodeUpdatePositionProps): void { - if (!this.positionInitialized || !this.nodeData.start) { - return - } - - const { - skipAnimation, - includeXPos, - } = options ?? {} - const { suppressMotion } = this.state - const { id } = this.nodeData - const { row } = this.layout.value[id] - - if (!this.layoutRows.value[row]) { - return - } - - const { yPos } = this.layoutRows.value[row] - const xPos = includeXPos ? this.state.timeScale.dateToX(this.nodeData.start) : this.position.x - - if (this.position.y === yPos && this.position.x === xPos) { - return - } - - if (skipAnimation || suppressMotion.value) { - this.position.set(xPos, yPos) - return - } - - gsap.to(this, { - x: xPos, - y: yPos, - duration: nodeAnimationDurations.move, - ease: 'power1.out', - }).then(() => { - this.state.cullScreen() - }) - } - - private updateIsRunningNode(): void { - this.isRunningNode = this.state.isRunning.value && !this.nodeData.end - } - - private updateBoxWidth(): void { - this.boxBody!.width = this.getBoxBodyWidth() - this.rightBoxCap?.position.set(this.nodeWidth, 0) - } - - private async updateSubNodesOutlineSize(skipAnimation?: boolean): Promise { - if (!this.subNodesOutline) { - return - } - - const { - isSubNodesExpanded, - subNodesOutline, - nodeHeight, - } = this - const { - spacingSubNodesOutlineOffset, - alphaSubNodesOutlineDimmed, - spacingNodeMargin, - } = this.state.styleOptions.value - const { suppressMotion } = this.state - - const width = this.getOutlineWidth() - const height = isSubNodesExpanded - ? nodeHeight + this.getSubContentHeight() + spacingNodeMargin - : nodeHeight - - subNodesOutline.resize({ - width, - height, - animate: true, - }) - - await new Promise((resolve) => { - const duration = skipAnimation || suppressMotion.value ? 0 : roundedBorderRectAnimationDuration - const xPos = isSubNodesExpanded ? -spacingSubNodesOutlineOffset : spacingSubNodesOutlineOffset - const yPos = isSubNodesExpanded ? -spacingSubNodesOutlineOffset : spacingSubNodesOutlineOffset - const alpha = isSubNodesExpanded ? 1 : alphaSubNodesOutlineDimmed - - if (skipAnimation || suppressMotion.value) { - subNodesOutline.position.set(xPos, yPos) - subNodesOutline.alpha = alpha - resolve(null) - return - } - - gsap.to(subNodesOutline, { - x: xPos, - y: yPos, - alpha, - duration, - ease: roundedBorderRectAnimationEase, - }).then(() => resolve(null)) - }) - } - - private updateLabelPosition(): void { - if (!this.label) { - return - } - - const { - state: graphState, - nodeWidth, - hasSubNodes, - isSubNodesToggleFloating, - subNodesToggleWidth, - } = this - const { - spacingNodeXPadding, - spacingNodeLabelMargin, - spacingNodeYPadding, - } = graphState.styleOptions.value - - const floatingLabelPosition = (): number => hasSubNodes && isSubNodesToggleFloating - ? nodeWidth + spacingNodeLabelMargin + subNodesToggleWidth + spacingNodeXPadding - : nodeWidth + spacingNodeLabelMargin - - const inBoxLabelPosition = (): number => hasSubNodes && !isSubNodesToggleFloating - ? subNodesToggleWidth + spacingNodeXPadding - : spacingNodeXPadding - - const labelXPos = this.isLabelInBox ? inBoxLabelPosition() : floatingLabelPosition() - - this.label.position.set(labelXPos, spacingNodeYPadding) - } - - private updateSelectedRingSize(): void { - const { width, height } = this.getSelectedRingSize() - this.selectedRing!.resize({ width, height }) - } - - private updateSubNodesTogglePosition(): void { - if (!this.subNodesToggle) { - return - } - - const { nodeWidth } = this - const { borderRadiusNode, spacingNodeLabelMargin } = this.state.styleOptions.value - - this.isSubNodesToggleFloating = this.subNodesToggleWidth + borderRadiusNode > nodeWidth - this.subNodesToggle.updateFloatingState(this.isSubNodesToggleFloating) - - this.subNodesToggle.position.x = this.isSubNodesToggleFloating - ? nodeWidth + spacingNodeLabelMargin - : 0 - } - - /** - * Utilities - */ - public readonly initializePosition = (): void => { - this.positionInitialized = true - this.updatePosition({ skipAnimation: true, includeXPos: true }) - } - - private getNodeWidth(): number { - const { isRunningNode, boxCapWidth, nodeData } = this - - if (!nodeData.start) { - return 0 - } - - const minimumWidth = isRunningNode ? boxCapWidth : boxCapWidth * 2 - const actualWidth = this.state.timeScale.dateToX(nodeData.end ?? new Date()) - this.state.timeScale.dateToX(nodeData.start) - - return actualWidth > minimumWidth ? actualWidth : minimumWidth - } - - private getNodeHeight(): number { - const { - textLineHeightDefault, - spacingNodeYPadding, - } = this.state.styleOptions.value - - return textLineHeightDefault + spacingNodeYPadding * 2 - } - - private getBoxBodyWidth(): number { - const { isRunningNode, nodeWidth, boxCapWidth } = this - - return isRunningNode - ? nodeWidth - boxCapWidth - : nodeWidth - boxCapWidth * 2 - } - - private getOutlineWidth(): number { - const { nodeWidth, isSubNodesExpanded } = this - const { spacingSubNodesOutlineOffset } = this.state.styleOptions.value - - if (isSubNodesExpanded) { - return nodeWidth + spacingSubNodesOutlineOffset * 2 - } - - const minimumWidth = this.boxCapWidth * 2 - const actualCollapsedWidth = nodeWidth - spacingSubNodesOutlineOffset * 2 - - return actualCollapsedWidth >= minimumWidth ? actualCollapsedWidth : minimumWidth - } - - private getLabelText(): string { - if (!this.hasSubNodes) { - return this.nodeData.label - } - - const { subNodeLabels } = this.state - const { subflowRunId } = this.nodeData - - return subNodeLabels.value.has(subflowRunId!) - ? subNodeLabels.value.get(subflowRunId!)! - : this.nodeData.label - } - - private checkIsLabelInBox(tolerance: number = 0): boolean { - const { hasSubNodes, isSubNodesToggleFloating, subNodesToggle } = this - - return hasSubNodes && !isSubNodesToggleFloating - ? this.apxLabelWidth + tolerance < this.nodeWidth - subNodesToggle!.width - : this.apxLabelWidth + tolerance < this.nodeWidth - } - - private getSelectedRingSize(): { width: number, height: number, margin: number } { - const { nodeWidth, nodeHeight, hasSubNodes, isSubNodesExpanded } = this - const { - spacingNodeSelectionMargin, - spacingNodeSelectionWidth, - spacingSubNodesOutlineBorderWidth, - spacingSubNodesOutlineOffset, - } = this.state.styleOptions.value - - // The margin compensates for RoundedBorderRect using an inset border - const margin = spacingNodeSelectionMargin + spacingNodeSelectionWidth - - const width = nodeWidth + margin * 2 - const height = hasSubNodes && !isSubNodesExpanded - ? nodeHeight + spacingSubNodesOutlineBorderWidth + spacingSubNodesOutlineOffset + margin * 2 - : nodeHeight + margin * 2 - - return { width, height, margin } - } - - private emitSelection(): void { - const { id, subflowRunId } = this.nodeData - - const nodeSelectionEvent: NodeSelectionEvent = { - id: this.hasSubNodes ? subflowRunId! : id, - type: this.hasSubNodes ? 'subFlowRun' : 'task', - } - - this.emit(nodeClickEvents.nodeDetails, nodeSelectionEvent) - } - - private readonly getSubNodesData = (): TimelineData => { - if (!this.nodeData.subflowRunId) { - return new Map() - } - - const { expandedSubNodes } = this.state - const subNodesData = expandedSubNodes.value.get(this.nodeData.subflowRunId)!.data - - return 'value' in subNodesData ? subNodesData.value : subNodesData - } - - private getSubContentHeight(): number { - const { spacingNodeMargin } = this.state.styleOptions.value - - if (this.subNodesContent?.height) { - return this.subNodesContent.height - } - - if (this.subNodesLoadingIndicator?.height) { - return this.subNodesLoadingIndicator.height + spacingNodeMargin - } - - if (this.noSubNodesMessage?.height) { - return this.noSubNodesMessage.height + spacingNodeMargin - } - - return 0 - } - - private emitSubNodesToggle(id?: string): void { - this.emit(nodeClickEvents.subNodesToggle, id ?? this.nodeData.subflowRunId) - } - - private destroySubNodesContent(): void { - if (this.subNodesContentTicker) { - this.state.pixiApp.ticker.remove(this.subNodesContentTicker) - this.subNodesContentTicker = null - } - - this.destroyNoSubNodesMessage() - this.destroySubNodesLoadingIndicator() - this.subNodesContent?.destroy() - this.subNodesContent = null - } - - private destroySubNodesLoadingIndicator(): void { - this.subNodesLoadingIndicator?.destroy() - this.subNodesLoadingIndicator = null - } - - private destroyNoSubNodesMessage(): void { - this.noSubNodesMessage?.destroy() - this.noSubNodesMessage = null - } - - private destroyRunningNodeTicker(): void { - if (this.runningNodeTicker) { - this.state.pixiApp.ticker.remove(this.runningNodeTicker) - this.runningNodeTicker = null - } - } - - private killTweens(): void { - gsap.killTweensOf([this, this.subNodesOutline]) - } - - public destroy(): void { - const { cull } = this.state - cull.remove(this.box) - if (this.label) { - cull.remove(this.label) - } - - this.killTweens() - - if (this.isSelected) { - this.emit(nodeClickEvents.nodeDetails, null) - } - - if (this.isSubNodesExpanded) { - this.emitSubNodesToggle() - } - - this.destroyRunningNodeTicker() - this.destroySubNodesContent() - - this.subNodesOutline?.destroy() - this.leftBoxCap?.destroy() - this.rightBoxCap?.destroy() - this.boxBody?.destroy() - this.box.destroy() - this.subNodesToggle?.destroy() - this.label?.destroy() - this.selectedRing?.destroy() - - this.unWatchers.forEach(unwatch => unwatch()) - - super.destroy.call(this) - } -} diff --git a/src/pixiFunctions/timelineNodes.ts b/src/pixiFunctions/timelineNodes.ts deleted file mode 100644 index 23e63081..00000000 --- a/src/pixiFunctions/timelineNodes.ts +++ /dev/null @@ -1,489 +0,0 @@ -import { Container, TextMetrics } from 'pixi.js' -import { watch, WatchStopHandle, ref, toRaw } from 'vue' -import { - NodeLayoutWorkerResponse, - NodeLayoutWorkerArgs, - GraphState, - NodesLayout, - NodeLayoutRow -} from '@/models' -import { - DeselectLayer, - TimelineEdge, - TimelineNode, - destroyNodeTextureCache, - nodeClickEvents, - nodeResizeEvent, - nodeAnimationDurations, - getBitmapFonts -} from '@/pixiFunctions' -import { TimelineData, TimelineItem } from '@/types/timeline' -// eslint-disable-next-line import/default -import LayoutWorker from '@/workers/nodeLayout.worker.ts?worker&inline' - -export const timelineUpdateEvent = 'timelineUpdateEvent' - -type TimelineNodesProps = { - nodeContentContainerName?: string, - isSubNodes?: boolean, - data: TimelineData, - state: GraphState, -} - -type EdgeRecord = { - edge: TimelineEdge, - sourceId: string, - targetId: string, -} - -export class TimelineNodes extends Container { - private readonly layoutWorker: Worker = new LayoutWorker() - - private readonly isSubNodes - private data: TimelineData - private readonly state: GraphState - - private readonly nodeContainer = new Container() - public readonly nodeRecords: Map = new Map() - private readonly layout = ref({}) - private readonly layoutRows = ref([]) - - private readonly edgeContainer = new Container() - private readonly edgeRecords: EdgeRecord[] = [] - - private readonly unWatchers: WatchStopHandle[] = [] - private isSelectionPathHighlighted = false - - public constructor({ - nodeContentContainerName, - isSubNodes, - data, - state, - }: TimelineNodesProps) { - super() - - if (nodeContentContainerName) { - this.nodeContainer.name = nodeContentContainerName - } - - this.isSubNodes = isSubNodes - this.data = data - this.state = state - - this.initDeselectLayer() - this.initLayoutWorker() - this.initWatchers() - } - - private initWatchers(): void { - const { - layoutSetting, - hideEdges, - selectedNodeId, - } = this.state - - this.unWatchers.push( - watch(layoutSetting, () => { - this.updateLayoutSetting() - }), - watch(hideEdges, () => { - this.updateHideEdges() - }), - watch(selectedNodeId, () => { - if (selectedNodeId.value && this.nodeRecords.has(selectedNodeId.value)) { - if (this.isSelectionPathHighlighted) { - this.unHighlightSelectedNodePath() - } - this.isSelectionPathHighlighted = true - this.highlightSelectedNodePath() - return - } - if ( - this.isSelectionPathHighlighted - && (!selectedNodeId.value || !this.nodeRecords.has(selectedNodeId.value)) - ) { - this.unHighlightSelectedNodePath() - } - }), - ) - } - - private async initLayoutWorker(): Promise { - const { - styleOptions, - timeScaleArgs, - layoutSetting, - centerViewport, - suppressMotion, - } = this.state - - const textStyles = await getBitmapFonts(styleOptions.value) - const { spacingMinimumNodeEdgeGap } = styleOptions.value - - const apxCharacterWidth = TextMetrics.measureText('M', textStyles.nodeTextStyles).width - - const layoutWorkerOptions: NodeLayoutWorkerArgs = { - data: toRaw(this.data), - timeScaleArgs, - spacingMinimumNodeEdgeGap, - apxCharacterWidth, - layoutSetting: layoutSetting.value, - } - - this.layoutWorker.onmessage = ({ data }: NodeLayoutWorkerResponse) => { - this.layout.value = data.layout - - this.renderLayout() - - if (this.isSelectionPathHighlighted) { - this.highlightSelectedNodePath() - } - - if (data.centerViewportAfter && !this.isSubNodes) { - // allow time for nodes to move to their new positions - setTimeout(() => { - centerViewport() - }, suppressMotion.value ? 10 : nodeAnimationDurations.move * 1000 * 1.1) - } - - this.emit(timelineUpdateEvent) - } - - this.layoutWorker.postMessage(layoutWorkerOptions) - } - - private initDeselectLayer(): void { - if (this.isSubNodes) { - return - } - - const { pixiApp, viewport } = this.state - - const deselectLayer = new DeselectLayer(pixiApp, viewport) - - this.addChild(deselectLayer) - - deselectLayer.on('click', () => { - this.emitNullSelection() - }) - } - - private renderLayout(): void { - const { layout } = this - const isInitialRender = this.nodeRecords.size === 0 - const newlyCreatedNodes: string[] = [] - - Object.keys(layout.value).forEach((nodeId) => { - const newNodeData = this.data.get(nodeId) - - if (!newNodeData) { - return - } - - if (this.nodeRecords.has(nodeId)) { - this.nodeRecords.get(nodeId)!.update(newNodeData) - } else { - this.createNode(newNodeData) - newlyCreatedNodes.push(nodeId) - } - }) - - this.updateLayoutRows() - - newlyCreatedNodes.forEach((nodeId) => { - this.nodeRecords.get(nodeId)!.initializePosition() - }) - - if (isInitialRender) { - this.addChild(this.edgeContainer) - this.addChild(this.nodeContainer) - - if (!this.isSubNodes) { - this.state.centerViewport({ skipAnimation: true }) - } - } - } - - private createNode(nodeData: TimelineItem): void { - const { state: state, layout, layoutRows } = this - const node = new TimelineNode({ - nodeData, - state, - layout, - layoutRows, - }) - - this.registerEmits(node) - this.nodeRecords.set(nodeData.id, node) - this.addNodeEdges(nodeData) - - this.nodeContainer.addChild(node) - - node.on(nodeResizeEvent, () => { - this.updateLayoutRows() - }) - } - - private addNodeEdges(nodeData: TimelineItem): void { - nodeData.upstream.forEach((upstreamDependency) => { - const sourceNode = this.nodeRecords.get(upstreamDependency) - const targetNode = this.nodeRecords.get(nodeData.id) - - if (!sourceNode || !targetNode) { - console.warn('timelineNodes: could not find source or target node for edge, skipping') - return - } - - const edge = new TimelineEdge({ - sourceNode, - targetNode, - state: this.state, - }) - - if (this.state.hideEdges.value) { - edge.renderable = false - } - - this.edgeRecords.push({ - edge, - sourceId: upstreamDependency, - targetId: nodeData.id, - }) - - this.edgeContainer.addChild(edge) - }) - } - - /** - * Update Functions - */ - public update(newData: TimelineData): void { - this.data = newData - - if (newData.size !== this.nodeRecords.size) { - const message: NodeLayoutWorkerArgs = { - data: toRaw(this.data), - } - - this.layoutWorker.postMessage(message) - return - } - - this.nodeRecords.forEach((nodeItem, nodeId) => { - const newNodeData = this.data.get(nodeId) - - if (newNodeData) { - nodeItem.update(newNodeData) - } - }) - - this.emit(timelineUpdateEvent) - } - - public updateHideEdges(): void { - const { hideEdges, viewport } = this.state - - this.edgeRecords.forEach(({ edge }) => edge.renderable = !hideEdges.value) - - if (!hideEdges.value) { - // the viewport needs to update transforms so the edges show in the right place - viewport.dirty = true - viewport.updateTransform() - } - - if (this.isSelectionPathHighlighted) { - this.highlightSelectedNodePath() - } - } - - private updateLayoutRows(position: number = 0): void { - const { layout } = this - const { spacingNodeMargin, spacingNodeSelectionMargin } = this.state.styleOptions.value - const maxRows = Math.max(...Object.values(layout.value).map(node => node.row)) - let newLayoutRows: NodeLayoutRow[] = [] - - const rowHeights = Object.keys(layout.value).reduce((acc, nodeId) => { - const { row } = layout.value[nodeId] - const currentRowHeight = acc.get(row) ?? 0 - const height = this.nodeRecords.get(nodeId)?.height ?? 0 - - acc.set(row, Math.max(height, currentRowHeight)) - - return acc - }, new Map()) - - for (let i = position; i <= maxRows; i++) { - const previousRow = newLayoutRows[i - 1] as NodeLayoutRow | undefined - const height = rowHeights.get(i) ?? 0 - - if (previousRow === undefined) { - newLayoutRows.push({ yPos: 0, height }) - continue - } - - const yPos = previousRow.yPos + previousRow.height - spacingNodeSelectionMargin * 2 + spacingNodeMargin - - newLayoutRows.push({ yPos, height }) - } - - if (position > 0) { - const combinedLayoutRows = this.layoutRows.value.slice(0, position).concat(newLayoutRows) - newLayoutRows = combinedLayoutRows - } - - this.layoutRows.value = newLayoutRows - } - - public updateLayoutSetting(): void { - const { layoutSetting } = this.state - - const message: NodeLayoutWorkerArgs = { - data: toRaw(this.data), - layoutSetting: layoutSetting.value, - centerViewportAfter: true, - } - - this.layoutWorker.postMessage(message) - } - - /** - * Node Selection - */ - private highlightSelectedNodePath(): void { - const selectedNodeId = this.state.selectedNodeId.value - const selectedNode = selectedNodeId && this.nodeRecords.get(selectedNodeId) - - if (!selectedNodeId || !selectedNode) { - return - } - - const { alphaNodeDimmed } = this.state.styleOptions.value - - const highlightedEdges = [ - ...this.getAllUpstreamEdges(selectedNodeId), - ...this.getAllDownstreamEdges(selectedNodeId), - ] - - const highlightedNodes: Map = new Map() - highlightedNodes.set(selectedNodeId, selectedNode) - - this.edgeRecords.forEach((edgeRecord) => { - if (highlightedEdges.includes(edgeRecord)) { - const upstreamNode = this.nodeRecords.get(edgeRecord.sourceId)! - const downstreamNode = this.nodeRecords.get(edgeRecord.targetId)! - highlightedNodes.set(edgeRecord.sourceId, upstreamNode) - highlightedNodes.set(edgeRecord.targetId, downstreamNode) - edgeRecord.edge.renderable = true - return - } - edgeRecord.edge.alpha = alphaNodeDimmed - }) - this.nodeRecords.forEach((nodeRecord, nodeRecordId) => { - if (highlightedNodes.has(nodeRecordId)) { - return - } - nodeRecord.alpha = alphaNodeDimmed - }) - } - - private getAllUpstreamEdges(nodeId: string): EdgeRecord[] { - const connectedEdges: EdgeRecord[] = [] - const nodeData = this.data.get(nodeId) - - nodeData?.upstream.forEach((upstreamId) => { - const edge = this.edgeRecords.find(edgeRecord => { - return edgeRecord.sourceId === upstreamId && edgeRecord.targetId === nodeId - }) - - if (edge) { - connectedEdges.push(edge) - const upstreamEdges = this.getAllUpstreamEdges(upstreamId) - connectedEdges.push(...upstreamEdges) - } - }) - - return connectedEdges - } - - private getAllDownstreamEdges(nodeId: string): EdgeRecord[] { - const connectedEdges: EdgeRecord[] = [] - - this.data.forEach((nodeData) => { - if (nodeData.upstream.includes(nodeId)) { - const edge = this.edgeRecords.find(edgeRecord => { - return edgeRecord.targetId === nodeData.id && edgeRecord.sourceId === nodeId - }) - if (edge) { - connectedEdges.push(edge) - const downstreamEdges = this.getAllDownstreamEdges(nodeData.id) - connectedEdges.push(...downstreamEdges) - } - } - }) - - return connectedEdges - } - - private unHighlightSelectedNodePath(): void { - const { hideEdges } = this.state - - this.edgeRecords.forEach(({ edge }) => { - if (hideEdges.value) { - edge.renderable = false - } - edge.alpha = 1 - }) - this.nodeRecords.forEach((nodeRecord) => { - nodeRecord.alpha = 1 - }) - } - - /** - * Utilities - */ - - private registerEmits(el: TimelineNode | TimelineNodes): void { - el.on(nodeClickEvents.nodeDetails, (nodeSelectionValue) => { - this.emit(nodeClickEvents.nodeDetails, nodeSelectionValue) - }) - el.on(nodeClickEvents.subNodesToggle, (id) => { - this.emit(nodeClickEvents.subNodesToggle, id) - }) - } - - private emitNullSelection(): void { - this.emit(nodeClickEvents.nodeDetails, null) - } - - public getEarliestNodeStart(): Date | null { - if (this.data.size < 1) { - return null - } - - let earliest: Date = new Date() - - this.data.forEach(({ start }) => { - if (start && start.getTime() < earliest.getTime()) { - earliest = start - } - }) - - return earliest - } - - public destroy(): void { - this.nodeRecords.forEach(nodeRecord => nodeRecord.destroy()) - this.nodeRecords.clear() - this.edgeRecords.forEach(edgeRecord => edgeRecord.edge.destroy()) - this.removeChildren() - this.unWatchers.forEach(unwatch => unwatch()) - this.layoutWorker.terminate() - this.layoutWorker.onmessage = null - - if (!this.isSubNodes) { - destroyNodeTextureCache() - } - - super.destroy.call(this) - } -} diff --git a/src/pixiFunctions/timelinePlayhead.ts b/src/pixiFunctions/timelinePlayhead.ts deleted file mode 100644 index a9399384..00000000 --- a/src/pixiFunctions/timelinePlayhead.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { BitmapText, Container, Graphics } from 'pixi.js' -import { watch, WatchStopHandle } from 'vue' -import { GraphState } from '@/models' -import { getBitmapFonts } from '@/pixiFunctions/bitmapFonts' -import { zIndex } from '@/utilities/zIndex' - -export class TimelinePlayhead extends Container { - private readonly state: GraphState - - private readonly unwatch: WatchStopHandle - - private readonly playhead = new Graphics() - private label: BitmapText | undefined - - public constructor(state: GraphState) { - super() - - this.state = state - - this.state.cull.add(this) - - this.zIndex = zIndex.playhead - - this.drawPlayhead() - - this.drawTimeLabel() - - this.unwatch = watch(this.state.styleOptions, () => { - this.playhead.clear() - this.drawPlayhead() - }) - - this.interactive = false - } - - private drawPlayhead(): void { - const { - colorPlayheadBg, - spacingPlayheadWidth, - spacingPlayheadGlowPadding, - } = this.state.styleOptions.value - - this.playhead.beginFill(colorPlayheadBg, 0.1) - this.playhead.drawRect( - 0, - 0, - spacingPlayheadWidth + spacingPlayheadGlowPadding * 2, - this.state.pixiApp.screen.height, - ) - this.playhead.endFill() - this.playhead.beginFill(colorPlayheadBg) - this.playhead.drawRect( - spacingPlayheadGlowPadding, - 0, - spacingPlayheadWidth, - this.state.pixiApp.screen.height, - ) - this.playhead.endFill() - - this.addChild(this.playhead) - } - - private async drawTimeLabel(): Promise { - const { - spacingGuideLabelPadding, - spacingPlayheadGlowPadding, - } = this.state.styleOptions.value - const textStyles = await getBitmapFonts(this.state.styleOptions.value) - const { timeBySeconds } = this.state.formatDateFns.value - const startDate = timeBySeconds(new Date()) - this.label = new BitmapText(startDate, textStyles.playheadTimerLabel) - - this.label.x = -this.label.width - (spacingPlayheadGlowPadding + spacingGuideLabelPadding) - this.label.y = this.getTimeLabelY() - this.addChild(this.label) - - setInterval(() => { - const date = new Date() - this.label!.text = timeBySeconds(date) - }, 1000) - } - - private getTimeLabelY(): number { - const { spacingGuideLabelPadding } = this.state.styleOptions.value - return this.state.pixiApp.screen.height - (this.label!.height + spacingGuideLabelPadding) - } - - public updatePosition(): void { - const { - spacingPlayheadWidth, - spacingPlayheadGlowPadding, - } = this.state.styleOptions.value - - this.position.x = - this.state.timeScale.dateToX(new Date()) * this.state.viewport.scale._x - + this.state.viewport.worldTransform.tx - - spacingPlayheadGlowPadding - - spacingPlayheadWidth / 2 - - if (this.playhead.height !== this.state.pixiApp.screen.height) { - this.playhead.height = this.state.pixiApp.screen.height - this.label!.y = this.getTimeLabelY() - } - } - - public destroy(): void { - this.state.cull.remove(this) - this.unwatch() - this.playhead.destroy() - super.destroy.call(this) - } -} diff --git a/src/pixiFunctions/viewport.ts b/src/pixiFunctions/viewport.ts deleted file mode 100644 index 43923a87..00000000 --- a/src/pixiFunctions/viewport.ts +++ /dev/null @@ -1,6 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export async function getPixiViewport() { - const { Viewport } = await import('pixi-viewport') - - return Viewport -} \ No newline at end of file diff --git a/src/styles/style.css b/src/styles/style.css index 5b0ba61d..7c05d4dd 100644 --- a/src/styles/style.css +++ b/src/styles/style.css @@ -1,2 +1,2 @@ @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index 1a656a2e..00000000 --- a/src/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './timeline' diff --git a/src/types/timeline.ts b/src/types/timeline.ts deleted file mode 100644 index 0eb8eda4..00000000 --- a/src/types/timeline.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type TimelineItem = { - id: string, - label: string, - state: string, - start: Date | null, - end: Date | null, - subflowRunId: string | null, - upstream: string[], - downstream: string[], -} - -export type TimelineData = Map \ No newline at end of file diff --git a/src/utilities/index.ts b/src/utilities/index.ts deleted file mode 100644 index c4a40803..00000000 --- a/src/utilities/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './map' -export * from './math' -export * from './style' -export * from './time' -export * from './zIndex' diff --git a/src/utilities/map.ts b/src/utilities/map.ts deleted file mode 100644 index ecacbea3..00000000 --- a/src/utilities/map.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function mapSome(map: Map, callback: (value: V, key: K) => boolean): boolean { - for (const [key, value] of map.entries()) { - if (callback(value, key)) { - return true - } - } - - return false -} diff --git a/src/utilities/math.ts b/src/utilities/math.ts deleted file mode 100644 index 737a8482..00000000 --- a/src/utilities/math.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint-disable id-length */ -/* eslint-disable no-nested-ternary */ -export const { - abs, - atan2, - cos, - max, - min, - sin, - tan, - sqrt, - pow, - floor, - ceil, - random, - round, -} = Math - -export const epsilon = 1e-12 -export const pi = Math.PI -export const halfPi = pi / 2 -export const tau = 2 * pi - -export const acos = (x: number): number => { - return x > 1 ? 0 : x < -1 ? pi : Math.acos(x) -} - -export const asin = (x: number): number => { - return x >= 1 ? halfPi : x <= -1 ? -halfPi : Math.asin(x) -} - -export const pow2 = (n: number): number => { - return n ** 2 -} - -export const choice = (list: T[] | Readonly): T => list[floor(random() * list.length)] - -export const range = (min: number, max: number): number[] => Array.from({ length: max - min }, (x, i) => min + i) - -export const uniform = (min: number, max: number): number => floor(random() * (max - min + 1) + min) - -export const coinflip = (weight: number): boolean => uniform(0, 1) < weight - -export const weightedNumber = (): number => { - const seed = uniform(1, 3) - - if (seed == 1) { - return choice(range(10, 100)) - } - - return choice(range(101, 1000)) -} \ No newline at end of file diff --git a/src/utilities/style.ts b/src/utilities/style.ts deleted file mode 100644 index e9766762..00000000 --- a/src/utilities/style.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { ParsedThemeStyles, ThemeStyleOverrides } from '@/models' -import { max, min, round } from '@/utilities/math' - -export function parseThemeOptions(overrides?: ThemeStyleOverrides): ParsedThemeStyles { - return { - colorTextDefault: colorToHex(overrides?.colorTextDefault ?? '#374151'), - colorTextInverse: colorToHex(overrides?.colorTextInverse ?? '#f8fafc'), - colorTextSubdued: colorToHex(overrides?.colorTextSubdued ?? '#6b7280'), - colorEdge: colorToHex(overrides?.colorEdge ?? '#374151'), - colorNodeSelection: colorToHex(overrides?.colorNodeSelection ?? '#024DFD'), - colorButtonBg: colorToHex(overrides?.colorButtonBg ?? '#ffffff'), - colorButtonBgHover: colorToHex(overrides?.colorButtonBgHover ?? '#cbd5e1'), - colorButtonBorder: overrides?.colorButtonBorder ? colorToHex(overrides.colorButtonBorder) : null, - colorGuideLine: colorToHex(overrides?.colorGuideLine ?? '#cbd5e1'), - colorPlayheadBg: colorToHex(overrides?.colorPlayheadBg ?? '#4e82fe'), - textFontFamilyDefault: overrides?.textFontFamilyDefault ?? 'InterVariable', - textSizeDefault: spacingToNumber(overrides?.textSizeDefault ?? '14px'), - textSizeSmall: spacingToNumber(overrides?.textSizeSmall ?? '12px'), - textLineHeightDefault: spacingToNumber(overrides?.textLineHeightDefault ?? '20px'), - textLineHeightSmall: spacingToNumber(overrides?.textLineHeightSmall ?? '16px'), - spacingButtonBorderWidth: spacingToNumber(overrides?.spacingButtonBorderWidth ?? '1px'), - spacingViewportPaddingDefault: spacingToNumber(overrides?.spacingViewportPaddingDefault ?? '40px'), - spacingNodeXPadding: spacingToNumber(overrides?.spacingNodeXPadding ?? '8px'), - spacingNodeYPadding: spacingToNumber(overrides?.spacingNodeYPadding ?? '8px'), - spacingNodeMargin: spacingToNumber(overrides?.spacingNodeMargin ?? '24px'), - spacingNodeLabelMargin: spacingToNumber(overrides?.spacingNodeLabelMargin ?? '20px'), - spacingMinimumNodeEdgeGap: spacingToNumber(overrides?.spacingMinimumNodeEdgeGap ?? '16px'), - spacingNodeEdgeLength: spacingToNumber(overrides?.spacingNodeEdgeLength ?? '8px'), - spacingNodeSelectionMargin: spacingToNumber(overrides?.spacingNodeSelectionMargin ?? '2px'), - spacingNodeSelectionWidth: spacingToNumber(overrides?.spacingNodeSelectionWidth ?? '4px'), - spacingSubNodesOutlineBorderWidth: spacingToNumber(overrides?.spacingSubNodesOutlineBorderWidth ?? '2px'), - spacingSubNodesOutlineOffset: spacingToNumber(overrides?.spacingSubNodesOutlineOffset ?? '5px'), - spacingEdgeWidth: spacingToNumber(overrides?.spacingEdgeWidth ?? '2px'), - spacingGuideLabelPadding: spacingToNumber(overrides?.spacingGuideLabelPadding ?? '4px'), - spacingPlayheadGlowPadding: spacingToNumber(overrides?.spacingPlayheadGlowPadding ?? '8px'), - spacingPlayheadWidth: spacingToNumber(overrides?.spacingPlayheadWidth ?? '2px'), - borderRadiusNode: spacingToNumber(overrides?.borderRadiusNode ?? '6px'), - borderRadiusButton: spacingToNumber(overrides?.borderRadiusButton ?? '6px'), - alphaNodeDimmed: overrides?.alphaNodeDimmed ?? 0.2, - alphaSubNodesOutlineDimmed: overrides?.alphaSubNodesOutlineDimmed ?? 0.7, - } -} - -export function colorToHex(color: string): number { - const trimmedColor = color.trim() - - if (trimmedColor.startsWith('rgb')) { - const [red, green, blue] = trimmedColor.replace(/[^\d,]/g, '').split(',').map((val) => parseInt(val, 10)) - return rgb2hex([red, green, blue]) - } - - if (trimmedColor.startsWith('hsl')) { - const [hue, saturation, lightness] = trimmedColor.replace(/[^\d,.%]/g, '').split(',').map((val) => parseInt(val, 10)) - const val = string2hex(hslToHex(hue, saturation, lightness)) - return val - } - - return string2hex(trimmedColor) -} - -function rgb2hex(rgb: number[] | Float32Array): number { - return parseInt((1 << 24 | rgb[0] << 16 | rgb[1] << 8 | rgb[2]).toString(16).slice(1), 16) -} - -function hslToHex(hue: number, saturation: number, lightness: number): string { - lightness /= 100 - const alpha = saturation * min(lightness, 1 - lightness) / 100 - - const hexValue = (num: number): string => { - const kar = (num + hue / 30) % 12 - const color = lightness - alpha * max(min(kar - 3, 9 - kar, 1), -1) - return round(255 * color).toString(16).padStart(2, '0') - } - - return `#${hexValue(0)}${hexValue(8)}${hexValue(4)}` -} - -function string2hex(string: string): number { - // @TODO: Once we update to v7, PIXI.utils.string2hex can handle this - // ticket to update to v7: https://github.com/PrefectHQ/graphs/issues/49 - // lifted from https://github.com/pixijs/pixijs/blob/dev/packages/utils/src/color/hex.ts - if (typeof string === 'string') { - - if (string.startsWith('#')) { - string = string.slice(1) - } - - if (string.length === 3) { - const [red, green, blue] = string - - string = red + red + green + green + blue + blue - } - } - - return parseInt(string, 16) -} - -export function spacingToNumber(spacing: string): number { - if (typeof spacing !== 'string') { - logSpacingToNumberError(spacing) - return 0 - } - - if (spacing.endsWith('em') || spacing.endsWith('rem')) { - return convertRemToNumber(spacing) - } else if (spacing.endsWith('px')) { - return parseFloat(spacing) - } - - logSpacingToNumberError(spacing) - return 0 -} -function logSpacingToNumberError(spacing: string): void { - console.error(` - FlowRunTimeline failed to parse spacing style: ${spacing}. - Make sure to use 'rem', 'em', or 'px' values. - Defaulting to zero. - `) -} - -function convertRemToNumber(rem: string): number { - const documentFontSize = getComputedStyle(document.documentElement).fontSize - return parseFloat(rem) * parseFloat(documentFontSize) -} diff --git a/src/utilities/time.ts b/src/utilities/time.ts deleted file mode 100644 index 677ed628..00000000 --- a/src/utilities/time.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { GuideDateFormatter } from '@/containers/guide' -import { FormatDateFns } from '@/models/FlowRunTimeline' -import { TimelineData } from '@/types/timeline' - -export const intervals = { - year: 31536000, - day: 86400, - hour: 3600, - minute: 60, - second: 1, -} as const - -type IntervalTypes = keyof typeof intervals -type IntervalTypesShort = 'y' | 'd' | 'h' | 'm' | 's' -type IntervalTypesPlural = `${keyof typeof intervals}s` - -function aggregateSeconds(input: number): Record { - const years = Math.floor(input / intervals.year) - const days = Math.floor(input % intervals.year / intervals.day) - const hours = Math.floor(input % intervals.year % intervals.day / intervals.hour) - const minutes = Math.floor(input % intervals.year % intervals.day % intervals.hour / intervals.minute) - const seconds = Math.ceil(input % intervals.year % intervals.day % intervals.hour % intervals.minute) - - return { years, days, hours, minutes, seconds } -} - -function intervalStringSeconds(seconds: number, showOnes = true): string { - return `${seconds === 1 && !showOnes ? '' : seconds}` -} - -function intervalStringIntervalType(type: IntervalTypes, seconds: number, showOnes = true): string { - return `${intervalStringSeconds(seconds, showOnes)} ${type}${seconds !== 1 ? 's' : ''}` -} - -function intervalStringSecondsIntervalTypeShort(type: IntervalTypesShort, seconds: number, showOnes = true): string { - return `${intervalStringSeconds(seconds, showOnes)}${type}` -} - -export function secondsToString(input: number, showOnes = true): string { - const { years, days, hours, minutes, seconds } = aggregateSeconds(input) - const year = years ? intervalStringIntervalType('year', years, showOnes) : '' - const day = days ? intervalStringIntervalType('day', days, showOnes) : '' - const hour = hours ? intervalStringIntervalType('hour', hours, showOnes) : '' - const minute = minutes ? intervalStringIntervalType('minute', minutes, showOnes) : '' - const second = seconds ? intervalStringIntervalType('second', seconds, showOnes) : '' - - return [year, day, hour, minute, second].map(x => x ? x : '').join(' ') -} - -export function secondsToApproximateString(input: number, showOnes = true): string { - const { years, days, hours, minutes, seconds } = aggregateSeconds(input) - const year = intervalStringSecondsIntervalTypeShort('y', years, showOnes) - const day = intervalStringSecondsIntervalTypeShort('d', days, showOnes) - const hour = intervalStringSecondsIntervalTypeShort('h', hours, showOnes) - const minute = intervalStringSecondsIntervalTypeShort('m', minutes, showOnes) - const second = intervalStringSecondsIntervalTypeShort('s', seconds, showOnes) - - switch (true) { - case years > 0 && days == 0: - return year - case years > 0 && days > 0: - return `${year} ${day}` - case days > 0 && hours == 0: - return day - case days > 0 && hours > 0: - return `${day} ${hour}` - case hours > 0 && minutes == 0: - return `${hour} ${minute}` - case hours > 0 && minutes > 0: - return `${hour} ${minute}` - case minutes > 0 && seconds == 0: - return minute - case minutes > 0 && seconds > 0: - return `${minute} ${second}` - default: - return second - } -} - -export function formatDateBySeconds(date: Date): string { - return date.toLocaleTimeString() -} - -export function formatDateByMinutes(date: Date): string { - const currentLocale = navigator.language - return new Intl.DateTimeFormat(currentLocale, { timeStyle: 'short' }).format(date) -} - -export function formatDate(date: Date): string { - const currentLocale = navigator.language - return new Intl.DateTimeFormat(currentLocale, { dateStyle: 'short' }).format(date) -} - -export function getDateBounds( - data: TimelineData, - minimumTimeSpan: number = 0, - isRunning: boolean = false, -): { min: Date, max: Date, span: number } { - let minStartTime: number | null = null - let maxEndTime: number | null = isRunning ? new Date().getTime() : null - - data.forEach(({ start, end }) => { - const startTime = start?.getTime() ?? null - const endTime = end?.getTime() ?? null - - if (startTime && minStartTime) { - minStartTime = Math.min(minStartTime, startTime) - } else { - minStartTime = startTime - } - - if (endTime && maxEndTime) { - maxEndTime = Math.max(maxEndTime, endTime) - } else { - maxEndTime = endTime - } - }) - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const min = minStartTime ? new Date(minStartTime) : new Date() - let max = maxEndTime ? new Date(maxEndTime) : new Date() - let span = max.getTime() - min.getTime() - - if (span < minimumTimeSpan) { - max = new Date(min.getTime() + minimumTimeSpan) - span = minimumTimeSpan - } - - return { - min, - max, - span, - } -} - -export function roundDownToNearestDay(date: Date): Date { - return new Date(date.getFullYear(), date.getMonth(), date.getDate()) -} - -export function roundDownToNearestEvenNumberedHour(date: Date): Date { - return new Date( - date.getFullYear(), - date.getMonth(), - date.getDate(), - Math.floor(date.getHours() / 2) * 2, - ) -} - -export const timeLengths = { - second: 1000, - minute: 1000 * 60, - hour: 1000 * 60 * 60, - day: 1000 * 60 * 60 * 24, - week: 1000 * 60 * 60 * 24 * 7, -} - -export const labelFormats = { - seconds: 'seconds', - minutes: 'minutes', - date: 'date', -} as const - -export type TimeSpan = { - ceiling: number, - span: number, - labelFormat: typeof labelFormats[keyof typeof labelFormats], -} - -export const timeSpanSlots: TimeSpan[] = [ - { - ceiling: timeLengths.second * 4, - span: timeLengths.second, - labelFormat: labelFormats.seconds, - }, { - ceiling: timeLengths.second * 8, - span: timeLengths.second * 5, - labelFormat: labelFormats.seconds, - }, { - ceiling: timeLengths.second * 13, - span: timeLengths.second * 10, - labelFormat: labelFormats.seconds, - }, { - ceiling: timeLengths.second * 20, - span: timeLengths.second * 15, - labelFormat: labelFormats.seconds, - }, { - ceiling: timeLengths.second * 45, - span: timeLengths.second * 30, - labelFormat: labelFormats.seconds, - }, { - ceiling: timeLengths.minute * 4, - span: timeLengths.minute, - labelFormat: labelFormats.minutes, - }, { - ceiling: timeLengths.minute * 8, - span: timeLengths.minute * 5, - labelFormat: labelFormats.minutes, - }, { - ceiling: timeLengths.minute * 13, - span: timeLengths.minute * 10, - labelFormat: labelFormats.minutes, - }, { - ceiling: timeLengths.minute * 28, - span: timeLengths.minute * 15, - labelFormat: labelFormats.minutes, - }, { - ceiling: timeLengths.hour * 1.24, - span: timeLengths.minute * 30, - labelFormat: labelFormats.minutes, - }, { - ceiling: timeLengths.hour * 3, - span: timeLengths.hour, - labelFormat: labelFormats.minutes, - }, { - ceiling: timeLengths.hour * 8, - span: timeLengths.hour * 2, - labelFormat: labelFormats.minutes, - }, { - ceiling: timeLengths.hour * 13, - span: timeLengths.hour * 6, - labelFormat: labelFormats.minutes, - }, { - ceiling: timeLengths.hour * 22, - span: timeLengths.hour * 12, - labelFormat: labelFormats.minutes, - }, { - ceiling: timeLengths.day * 4, - span: timeLengths.day, - labelFormat: labelFormats.date, - }, { - ceiling: timeLengths.week * 2, - span: timeLengths.week, - labelFormat: labelFormats.date, - }, { - ceiling: Infinity, - span: timeLengths.week * 4, - labelFormat: labelFormats.date, - }, -] - -export function getTimeSpanSlot(span: number): TimeSpan { - return timeSpanSlots.find(timeSlot => timeSlot.ceiling > span)! -} - -export function getLabelFormatter(labelFormat: string, formatters: FormatDateFns): GuideDateFormatter { - switch (labelFormat) { - case labelFormats.minutes: - return (value: Date) => formatByMinutesWithDates(value, formatters) - case labelFormats.date: - return formatters.date - default: - return formatters.timeBySeconds - } -} - -function formatByMinutesWithDates(date: Date, formatters: FormatDateFns): string { - if (date.getHours() === 0 && date.getMinutes() === 0) { - return `${formatters.date(date)}\n${formatters.timeByMinutes(date)}` - } - - return formatters.timeByMinutes(date) -} \ No newline at end of file diff --git a/src/utilities/viewport.ts b/src/utilities/viewport.ts deleted file mode 100644 index 9f334b21..00000000 --- a/src/utilities/viewport.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Viewport } from 'pixi-viewport' - -export type ViewportUpdatedCheck = () => boolean - -export function viewportUpdatedFactory(viewport: Viewport): ViewportUpdatedCheck { - let { left: previousLeft, right: previousRight, height: previousHeight } = viewport - - function updated(): boolean { - const { left, right, height } = viewport - - if (left === previousLeft && right === previousRight && height === previousHeight) { - return false - } - - previousLeft = left - previousRight = right - previousHeight = height - - return true - } - - return updated -} \ No newline at end of file diff --git a/src/utilities/zIndex.ts b/src/utilities/zIndex.ts deleted file mode 100644 index bb55b1a2..00000000 --- a/src/utilities/zIndex.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const zIndex = { - timelineGuides: 0, - viewport: 10, - playhead: 20, -} as const \ No newline at end of file diff --git a/src/workers/layouts/nearestNeighbor.ts b/src/workers/layouts/nearestNeighbor.ts deleted file mode 100644 index 946b7e06..00000000 --- a/src/workers/layouts/nearestNeighbor.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { NodeLayoutItem, NodeShoveDirection, NodesLayout, TimeScale } from '@/models' -import { TimelineData, TimelineItem } from '@/types/timeline' - -const DEFAULT_POSITION = 0 - -type FactoryArgs = { - data: TimelineData, - timeScale: TimeScale, - currentApxCharacterWidth: number, - minimumNodeEdgeGap: number, -} - -type ShoveProps = { - direction: NodeShoveDirection, - nodeStartX: number, - desiredPosition: number, -} - -type IsNodesOverlappingProps = { - firstNodeEndX: number, - firstNodePosition: number, - lastNodeStartX: number, - lastNodePosition: number, -} - -type ArgueWithCompetingUpstreamPlacementProps = { - desiredPosition: number, - nodeStartX: number, - upstreamPositions: number[], - competingLayoutItemId: string, -} - -export async function generateNearestParentLayout({ - data, - timeScale, - currentApxCharacterWidth, - minimumNodeEdgeGap, -}: FactoryArgs): Promise { - const layout: NodesLayout = {} - - async function generate(): Promise { - for await (const [, item] of data) { - if (!item.start) { - continue - } - - const endAsPx = timeScale.dateToX(item.end ?? new Date()) - // Accommodate the label width so they don't overlap - const apxLabelWidth = item.label.length * currentApxCharacterWidth - const endX = endAsPx + apxLabelWidth - - const startX = timeScale.dateToX(new Date(item.start)) - - const row = await getNearestParentRow(item, startX) - - layout[item.id] = { - row, - startX, - endX, - } - } - } - - async function getNearestParentRow(nodeData: TimelineItem, nodeStartX: number): Promise { - // if one dependency - if (nodeData.upstream.length === 1) { - if (nodeData.upstream[0] in layout) { - const parent = layout[nodeData.upstream[0]] - return await placeNearUpstreamNode(parent, nodeStartX) - } - - console.warn('timelineNodes layout worker: Parent node not found in layout', nodeData.upstream[0]) - return DEFAULT_POSITION - } - - // if more than one dependency – add to the middle of upstream dependencies - if (nodeData.upstream.length > 0) { - const upstreamLayoutItems = nodeData.upstream - .map(id => layout[id]) - .filter((layoutItem: NodeLayoutItem | undefined): layoutItem is NodeLayoutItem => !!layoutItem) - const upstreamPositions = upstreamLayoutItems.map(layoutItem => layoutItem.row) - const upstreamPositionSum = upstreamPositions.reduce((sum, position) => sum + position, 0) - const upstreamPositionAverage = upstreamPositionSum / upstreamPositions.length - - const position = Math.round(upstreamPositionAverage) - - if (isPositionTaken(nodeStartX, position)) { - const overlappingLayoutIds = getOverlappingLayoutIds( - nodeStartX, - position, - )! - - const upstreamDependenciesOverlapping = overlappingLayoutIds.filter(layoutId => { - return nodeData.upstream.includes(layoutId) - }) - - if (upstreamDependenciesOverlapping.length > 0 || overlappingLayoutIds.length > 1) { - // upstream nodeData dependencies always win, or if there are more than one node in the way - const [upstreamLayoutItemId] = upstreamDependenciesOverlapping.length > 0 - ? upstreamDependenciesOverlapping - : overlappingLayoutIds - const upstreamLayoutItem = layout[upstreamLayoutItemId] - - upstreamLayoutItem.nextDependencyShove = getShoveDirectionWeightedByDependencies( - upstreamPositions, - position, - upstreamLayoutItem.nextDependencyShove, - ) - - return await placeNearUpstreamNode(upstreamLayoutItem, nodeStartX) - } - - return await argueWithCompetingUpstreamPlacement({ - competingLayoutItemId: overlappingLayoutIds[0], - upstreamPositions, - nodeStartX, - desiredPosition: position, - }) - } - } - - // if zero dependencies - return placeRootNode(nodeStartX, DEFAULT_POSITION) - } - - function placeRootNode(nodeStartX: number, DEFAULT_POSITION: number): number { - if (isPositionTaken(nodeStartX, DEFAULT_POSITION)) { - return placeRootNode(nodeStartX, DEFAULT_POSITION + 1) - } - - return DEFAULT_POSITION - } - - async function placeNearUpstreamNode(upstreamLayoutItem: NodeLayoutItem, nodeStartX: number): Promise { - // See this diagram for how shove logic works in this scenario - // https://www.figma.com/file/1u1oXkiYRxgtqWSRG9Yely/DAG-Design?node-id=385%3A2782&t=yRLIggko0TzbMaIG-4 - if (upstreamLayoutItem.nextDependencyShove !== 1 && upstreamLayoutItem.nextDependencyShove !== -1) { - upstreamLayoutItem.nextDependencyShove = 1 - } - - const { - row: upstreamNodePosition, - nextDependencyShove, - } = upstreamLayoutItem - if (isPositionTaken(nodeStartX, upstreamNodePosition)) { - if (nextDependencyShove === 1 && !isPositionTaken(nodeStartX, upstreamNodePosition + 1)) { - upstreamLayoutItem.nextDependencyShove = -1 - return upstreamNodePosition + 1 - } else if (!isPositionTaken(nodeStartX, upstreamNodePosition - 1)) { - upstreamLayoutItem.nextDependencyShove = 1 - return upstreamNodePosition - 1 - } - await shove({ - direction: nextDependencyShove, - nodeStartX, - desiredPosition: upstreamNodePosition + nextDependencyShove, - }) - upstreamLayoutItem.nextDependencyShove = nextDependencyShove === 1 ? -1 : 1 - return upstreamNodePosition + nextDependencyShove - } - return upstreamNodePosition - } - - function isPositionTaken(nodeStartX: number, position: number): boolean { - const layoutKeys = Object.keys(layout) - return layoutKeys.length > 0 && layoutKeys.some((nodeId) => { - const layoutItem = layout[nodeId] - return isNodesOverlapping({ - firstNodeEndX: layoutItem.endX, - firstNodePosition: layoutItem.row, - lastNodeStartX: nodeStartX, - lastNodePosition: position, - }) - }) - } - - async function shove({ direction, nodeStartX, desiredPosition }: ShoveProps): Promise { - const overlappingLayoutIds = getOverlappingLayoutIds(nodeStartX, desiredPosition) - - if (!overlappingLayoutIds) { - return - } - - for await (const overlapId of overlappingLayoutIds) { - // push nodes and recursively shove as needed - const layoutItem = layout[overlapId] - const newPosition = layoutItem.row + direction - await shove({ - direction, - nodeStartX: layoutItem.startX, - desiredPosition: newPosition, - }) - layoutItem.row = newPosition - } - } - - function isNodesOverlapping({ - firstNodeEndX, - firstNodePosition, - lastNodeStartX, - lastNodePosition, - }: IsNodesOverlappingProps): boolean { - return firstNodePosition === lastNodePosition - && firstNodeEndX + minimumNodeEdgeGap >= lastNodeStartX - } - - function getOverlappingLayoutIds(nodeStartX: number, position: number): string[] | undefined { - const overlappingLayoutItems: string[] = [] - - Object.keys(layout).forEach(itemId => { - const layoutItem = layout[itemId] - - const isItemOverlapping = isNodesOverlapping({ - firstNodeEndX: layoutItem.endX, - firstNodePosition: layoutItem.row, - lastNodeStartX: nodeStartX, - lastNodePosition: position, - }) - - if (isItemOverlapping) { - overlappingLayoutItems.push(itemId) - } - }) - - if (overlappingLayoutItems.length === 0) { - return - } - - // sort last to first - overlappingLayoutItems.sort((itemAId, itemBId) => { - const itemA = layout[itemAId] - const itemB = layout[itemBId] - if (itemA.endX < itemB.endX) { - return 1 - } - if (itemA.endX > itemB.endX) { - return -1 - } - return 0 - }) - - return overlappingLayoutItems - } - - function getShoveDirectionWeightedByDependencies( - upstreamPositions: number[], - position: number, - defaultGravity?: NodeShoveDirection, - ): NodeShoveDirection { - // check if nodeData has more connections above or below, prefer placement in that direction - const upwardConnections = upstreamPositions.filter((upstreamPosition) => upstreamPosition < position).length - const downwardConnections = upstreamPositions.filter((upstreamPosition) => upstreamPosition > position).length - - if (upwardConnections > downwardConnections) { - return -1 - } - - return defaultGravity ?? 1 - } - async function argueWithCompetingUpstreamPlacement({ - desiredPosition, - nodeStartX, - upstreamPositions, - competingLayoutItemId, - }: ArgueWithCompetingUpstreamPlacementProps): Promise { - const competitor = layout[competingLayoutItemId] - const [ - competitorAboveConnections, - competitorBelowConnections, - ] = getLayoutItemUpAndDownwardConnections(competingLayoutItemId) - const nodeAboveConnections = upstreamPositions.filter((upstreamPosition) => upstreamPosition < desiredPosition).length - const nodeBelowConnections = upstreamPositions.filter((upstreamPosition) => upstreamPosition > desiredPosition).length - - if (nodeAboveConnections > nodeBelowConnections) { - // node has more above - if ( - competitorAboveConnections > competitorBelowConnections && competitorAboveConnections > nodeAboveConnections - ) { - // competitor has more above than below, and more above than node - // node wins, shove competitor up - await shove({ - direction: -1, - nodeStartX, - desiredPosition, - }) - return desiredPosition - } - if (competitorBelowConnections > competitorAboveConnections) { - // competitor has more below than above - // node wins, shove competitor down - await shove({ - direction: 1, - nodeStartX, - desiredPosition, - }) - return desiredPosition - } - - // competitor has equal above and below, or node has more above - // place node above competitor - competitor.nextDependencyShove = -1 - } - - if (nodeBelowConnections > nodeAboveConnections) { - // node has more below - if ( - competitorBelowConnections > competitorAboveConnections && competitorBelowConnections > nodeBelowConnections - ) { - // competitor has more below than above, and more below than node - // node wins, shove competitor down - await shove({ - direction: 1, - nodeStartX, - desiredPosition, - }) - return desiredPosition - } - - if (competitorAboveConnections > competitorBelowConnections) { - // competitor has more above than below - // node wins, shove competitor up - await shove({ - direction: -1, - nodeStartX, - desiredPosition, - }) - return desiredPosition - } - - // competitor has equal above and below, or node has more below - // place node below competitor - competitor.nextDependencyShove = 1 - } - - return await placeNearUpstreamNode(competitor, nodeStartX) - } - - function getLayoutItemUpAndDownwardConnections(id: string): [number, number] { - const connections = data.get(id)! - const layoutItem = layout[id] - - return connections.upstream.reduce((counts, dependencyId) => { - if (id in layout) { - const dependencyLayoutItem = layout[dependencyId] - - if (dependencyLayoutItem.row < layoutItem.row) { - counts[0] += 1 - } - - if (dependencyLayoutItem.row > layoutItem.row) { - counts[1] += 1 - } - - return counts - } - - console.warn('nodeLayout.worker.ts: Parent node not found on layout data', id) - return counts - }, [0, 0]) - } - - await generate() - - return layout - -} diff --git a/src/workers/layouts/waterfall.ts b/src/workers/layouts/waterfall.ts deleted file mode 100644 index 0613e626..00000000 --- a/src/workers/layouts/waterfall.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NodesLayout } from '@/models' -import { TimelineData } from '@/types/timeline' - -export function generateWaterfallLayout(data: TimelineData): NodesLayout { - const layout: NodesLayout = {} - let index = 0 - - data.forEach((item, id) => { - layout[id] = { - row: index, - startX: 0, - endX: 0, - } - - index++ - }) - - return layout -} \ No newline at end of file diff --git a/src/workers/nodeLayout.worker.ts b/src/workers/nodeLayout.worker.ts deleted file mode 100644 index f0d4ae92..00000000 --- a/src/workers/nodeLayout.worker.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - TimelineNodesLayoutOptions, - NodeLayoutWorkerArgs, - NodesLayout, - TimeScale, - NodeLayoutWorkerResponseData -} from '@/models' -import { createTimeScale } from '@/pixiFunctions/timeScale' -import { TimelineData } from '@/types/timeline' -import { generateNearestParentLayout } from '@/workers/layouts/nearestNeighbor' -import { generateWaterfallLayout } from '@/workers/layouts/waterfall' - -let timeScale: TimeScale | undefined - -let currentApxCharacterWidth = 14 -let minimumNodeEdgeGap = 0 -let currentLayoutSetting: TimelineNodesLayoutOptions | undefined -let graphDataStore: TimelineData = new Map() - -let layout: NodesLayout = {} - -onmessage = async ({ - data: { - layoutSetting, - data, - apxCharacterWidth, - spacingMinimumNodeEdgeGap, - timeScaleArgs, - centerViewportAfter, - }, -}: { data: NodeLayoutWorkerArgs }) => { - if (spacingMinimumNodeEdgeGap) { - minimumNodeEdgeGap = spacingMinimumNodeEdgeGap - } - - if (layoutSetting) { - currentLayoutSetting = layoutSetting - } - - if (apxCharacterWidth) { - currentApxCharacterWidth = apxCharacterWidth - } - - if (timeScaleArgs) { - const { - minimumStartTime, - graphXDomain, - initialOverallTimeSpan, - } = timeScaleArgs - timeScale = createTimeScale({ - minimumStartTime, - graphXDomain, - initialOverallTimeSpan, - }) - } - if (data && timeScale && currentLayoutSetting) { - graphDataStore = data - await calculateNodeLayout() - const response: NodeLayoutWorkerResponseData = { - layout, - centerViewportAfter, - } - - postMessage(response) - } -} - -async function calculateNodeLayout(): Promise { - if (currentLayoutSetting === 'waterfall') { - layout = generateWaterfallLayout(graphDataStore) - } - - if (currentLayoutSetting === 'nearestParent') { - layout = await generateNearestParentLayout({ - data: graphDataStore, - timeScale: timeScale!, - currentApxCharacterWidth, - minimumNodeEdgeGap, - }) - } - - purgeNegativePositions() -} - -function purgeNegativePositions(): void { - const lowestPosition = Object.values(layout).reduce((lowest, layoutItem) => { - if (layoutItem.row < lowest) { - return layoutItem.row - } - return lowest - }, 0) - - if (lowestPosition < 0) { - Object.values(layout).forEach(layoutItem => { - layoutItem.row += Math.abs(lowestPosition) - }) - } -} \ No newline at end of file diff --git a/src/workers/tsconfig.json b/src/workers/tsconfig.json deleted file mode 100644 index fa652af6..00000000 --- a/src/workers/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig", - "compilerOptions": { - "lib": [ - "WebWorker", - "esnext" - ] - }, - "files": [ - "nodeLayout.worker.ts" - ] -} From 87d319491aec4d98c5e92988bb63767829e2b6c6 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Wed, 27 Sep 2023 13:49:31 -0500 Subject: [PATCH 07/10] Fix missing type --- demo/components/AppComponentNavigationItems.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/demo/components/AppComponentNavigationItems.vue b/demo/components/AppComponentNavigationItems.vue index 08250bfb..a9de82b4 100644 --- a/demo/components/AppComponentNavigationItems.vue +++ b/demo/components/AppComponentNavigationItems.vue @@ -6,10 +6,14 @@