From dbad7493becd90b20a47b73bd6c35717681145ad Mon Sep 17 00:00:00 2001 From: Zyie <24736175+Zyie@users.noreply.github.com> Date: Wed, 11 Sep 2024 20:14:19 +0100 Subject: [PATCH] fix: only gather scene updates when needed --- .../src/assets/gpuTextures/textures.ts | 18 +- packages/backend/src/handler.ts | 17 + packages/backend/src/pixi.ts | 142 +- packages/backend/src/rendering/rendering.ts | 19 +- packages/backend/src/scene/overlay/overlay.ts | 43 +- packages/backend/src/scene/scene.ts | 73 ++ packages/backend/src/scene/stats/stats.ts | 41 +- packages/backend/src/scene/tree/properties.ts | 26 +- packages/backend/src/scene/tree/tree.ts | 151 ++- .../src/devtools/panel/panel.tsx | 2 +- packages/devtool-chrome/src/inject/close.ts | 4 +- packages/example/vite.config.ts | 3 + packages/frontend/src/App.tsx | 35 +- .../src/components/smooth-charts/Smoothie.tsx | 312 ----- .../smooth-charts/vendor-smoothie.ts | 1148 ----------------- .../pages/assets/gpuTextures/TextureStats.tsx | 22 - .../frontend/src/pages/scene/ScenePanel.tsx | 96 +- .../frontend/src/pages/scene/SceneStats.tsx | 42 + .../SceneProperties.tsx} | 4 +- .../Tree.tsx => graph/SceneTree.tsx} | 14 +- .../tree/context-menu-button.tsx | 2 +- .../{scene-section => graph}/tree/cursor.tsx | 0 .../tree/node-button.tsx | 4 +- .../tree/node-trigger.tsx | 4 +- .../{scene-section => graph}/tree/node.tsx | 0 .../tree/simple-tree.tsx | 12 +- .../src/pages/scene/{state.ts => scene.ts} | 23 +- .../src/pages/scene/stats-section/Stats.tsx | 127 -- packages/frontend/src/types.ts | 10 +- tsconfig.json | 2 + 30 files changed, 490 insertions(+), 1906 deletions(-) create mode 100644 packages/backend/src/handler.ts create mode 100644 packages/backend/src/scene/scene.ts delete mode 100644 packages/frontend/src/components/smooth-charts/Smoothie.tsx delete mode 100644 packages/frontend/src/components/smooth-charts/vendor-smoothie.ts create mode 100644 packages/frontend/src/pages/scene/SceneStats.tsx rename packages/frontend/src/pages/scene/{scene-section/Properties.tsx => graph/SceneProperties.tsx} (97%) rename packages/frontend/src/pages/scene/{scene-section/Tree.tsx => graph/SceneTree.tsx} (90%) rename packages/frontend/src/pages/scene/{scene-section => graph}/tree/context-menu-button.tsx (89%) rename packages/frontend/src/pages/scene/{scene-section => graph}/tree/cursor.tsx (100%) rename packages/frontend/src/pages/scene/{scene-section => graph}/tree/node-button.tsx (89%) rename packages/frontend/src/pages/scene/{scene-section => graph}/tree/node-trigger.tsx (94%) rename packages/frontend/src/pages/scene/{scene-section => graph}/tree/node.tsx (100%) rename packages/frontend/src/pages/scene/{scene-section => graph}/tree/simple-tree.tsx (79%) rename packages/frontend/src/pages/scene/{state.ts => scene.ts} (74%) delete mode 100644 packages/frontend/src/pages/scene/stats-section/Stats.tsx diff --git a/packages/backend/src/assets/gpuTextures/textures.ts b/packages/backend/src/assets/gpuTextures/textures.ts index 6c4ff1f..a6293ec 100644 --- a/packages/backend/src/assets/gpuTextures/textures.ts +++ b/packages/backend/src/assets/gpuTextures/textures.ts @@ -1,6 +1,6 @@ import type { TextureDataState } from '@devtool/frontend/pages/assets/assets'; -import type { PixiDevtools } from '../../pixi'; -import type { TextureSource, GlTexture, CanvasSource, WebGLRenderer, WebGPURenderer } from 'pixi.js'; +import type { CanvasSource, GlTexture, TextureSource, WebGLRenderer, WebGPURenderer } from 'pixi.js'; +import { PixiHandler } from '../../handler'; const gpuTextureFormatSize: Record = { r8unorm: 1, @@ -63,24 +63,22 @@ const glTextureFormatSize: Record = { 34041: 2, }; -export class Textures { - private _devtool: typeof PixiDevtools; +export class Textures extends PixiHandler { private _textures: Map = new Map(); private _gpuTextureSize: Map = new Map(); private _canvas = document.createElement('canvas'); - constructor(devtool: typeof PixiDevtools) { - this._devtool = devtool; + public override init() { + this._textures.clear(); + this._gpuTextureSize.clear(); } - public init() { + public override reset() { this._textures.clear(); this._gpuTextureSize.clear(); } - public update() {} - - public complete() {} + public override update() {} public get() { const currentTextures = this._devtool.renderer.texture.managedTextures; diff --git a/packages/backend/src/handler.ts b/packages/backend/src/handler.ts new file mode 100644 index 0000000..9fdcf52 --- /dev/null +++ b/packages/backend/src/handler.ts @@ -0,0 +1,17 @@ +import type { Container } from 'pixi.js'; +import type { PixiDevtools } from './pixi'; + +export class PixiHandler { + protected _devtool: typeof PixiDevtools; + constructor(devtool: typeof PixiDevtools) { + this._devtool = devtool; + } + + public init() {} + public reset() {} + public preupdate() {} + public update() {} + public throttledUpdate() {} + public loop(_container: Container) {} + public postupdate() {} +} diff --git a/packages/backend/src/pixi.ts b/packages/backend/src/pixi.ts index c719208..b0b12f1 100644 --- a/packages/backend/src/pixi.ts +++ b/packages/backend/src/pixi.ts @@ -1,11 +1,12 @@ -import type { DevtoolState } from '@devtool/frontend/types'; +import type { GlobalDevtoolState } from '@devtool/frontend/types'; import { DevtoolMessage } from '@devtool/frontend/types'; import type { Devtools } from '@pixi/devtools'; import type { Application, Container, Renderer, WebGLRenderer } from 'pixi.js'; +import { Textures } from './assets/gpuTextures/textures'; import { extensions } from './extensions/Extensions'; -import { Overlay } from './scene/overlay/overlay'; +import { Rendering } from './rendering/rendering'; import { overlayExtension } from './scene/overlay/overlayExtension'; -import { Stats } from './scene/stats/stats'; +import { Scene } from './scene/scene'; import { pixiStatsExtension, totalStatsExtension } from './scene/stats/statsExtension'; import { animatedSpritePropertyExtension } from './scene/tree/extensions/animatedSprite/animatedSpritePropertyExtension'; import { containerPropertyExtension } from './scene/tree/extensions/container/containerPropertyExtension'; @@ -13,14 +14,8 @@ import { nineSlicePropertyExtension } from './scene/tree/extensions/ninesliceSpr import { textPropertyExtension } from './scene/tree/extensions/text/textPropertyExtension'; import { tilingSpritePropertyExtension } from './scene/tree/extensions/tilingSprite/tilingSpritePropertyExtension'; import { viewPropertyExtension } from './scene/tree/extensions/view/viewPropertyExtension'; -import { Properties } from './scene/tree/properties'; -import { Tree } from './scene/tree/tree'; import { loop } from './utils/loop'; import { Throttle } from './utils/throttle'; -import { Textures } from './assets/gpuTextures/textures'; -import type { TextureState } from '@devtool/frontend/pages/assets/assets'; -import { Rendering } from './rendering/rendering'; -import type { RenderingState } from '@devtool/frontend/pages/rendering/rendering'; /** * PixiWrapper is a class that wraps around the PixiJS library. @@ -31,74 +26,16 @@ class PixiWrapper { public settings = { throttle: 100, }; - public state: Omit< - DevtoolState, - | 'active' - | 'setActive' - | 'bridge' - | 'setBridge' - | 'chromeProxy' - | 'setChromeProxy' - | 'reset' - | keyof TextureState - | keyof RenderingState - > = { + public state: Omit = { version: null, - setVersion: function (version: DevtoolState['version']) { + setVersion: function (version: GlobalDevtoolState['version']) { this.version = version; }, - - sceneGraph: null, - setSceneGraph: function (sceneGraph: DevtoolState['sceneGraph']) { - this.sceneGraph = sceneGraph ?? { - id: 'root', - name: 'root', - children: [], - metadata: { - type: 'Container', - }, - }; - }, - - sceneTreeData: { - buttons: [], - }, - setSceneTreeData: function (data: DevtoolState['sceneTreeData']) { - this.sceneTreeData = data; - }, - - stats: null, - setStats: function (stats: DevtoolState['stats']) { - this.stats = stats; - }, - - selectedNode: null, - setSelectedNode: function (selectedNode: DevtoolState['selectedNode']) { - this.selectedNode = selectedNode; - }, - - activeProps: [], - setActiveProps: function (activeProps: DevtoolState['activeProps']) { - this.activeProps = activeProps; - }, - - overlayPickerEnabled: false, - setOverlayPickerEnabled: function (enabled: DevtoolState['overlayPickerEnabled']) { - this.overlayPickerEnabled = enabled; - }, - - overlayHighlightEnabled: true, - setOverlayHighlightEnabled: function (enabled: DevtoolState['overlayHighlightEnabled']) { - this.overlayHighlightEnabled = enabled; - }, }; - public stats = new Stats(this); - public tree = new Tree(this); - public properties = new Properties(this); - public overlay = new Overlay(this); public textures = new Textures(this); public rendering = new Rendering(this); + public scene = new Scene(this); // Private properties private _devtools: Devtools | undefined; private _app: Application | undefined; @@ -240,6 +177,9 @@ class PixiWrapper { return this._version; } + /** + * Gets the major version of PixiJS. + */ public get majorVersion() { if (this.version === '') { if (!this.stage) { @@ -264,6 +204,9 @@ class PixiWrapper { return this.app || (this.stage && this.renderer) ? DevtoolMessage.active : DevtoolMessage.inactive; } + /** + * Gets the type of renderer being used. + */ public get rendererType(): 'webgl' | 'webgl2' | 'webgpu' | null { if (!this.renderer) return null; return this.renderer.type === 0b10 @@ -273,6 +216,9 @@ class PixiWrapper { : 'webgl2'; } + /** + * Inject into the renderers render method. + */ public inject() { // eslint-disable-next-line @typescript-eslint/no-this-alias const that = this; @@ -295,6 +241,8 @@ class PixiWrapper { this.renderer && this._originalRenderFn && (this.renderer.render = this._originalRenderFn); this.renderer && (this.renderer.__devtoolInjected = false); this.rendering.reset(); + this.scene.reset(); + this.textures.reset(); this._resetState(); this._devtools = undefined; this._app = undefined; @@ -312,21 +260,17 @@ class PixiWrapper { this.init(); } - // TODO: tree: 300ms, stats: 300ms, properties: 300ms, overlay: 50ms - - this.overlay.update(); - this.rendering.update(); + this.preupdate(); + this._update(); if (this._updateThrottle.shouldExecute(this.settings.throttle)) { - this.preupdate(); - + this.updatedThrottled(); // check if we are accessing the correct stage if (this.renderer!.lastObjectRendered === this.stage) { // loop the scene graph loop({ container: this.stage!, loop: (container) => { - this.stats.update(container); - this.tree.update(container); + this.updateLoop(container); }, test: (container) => { if (container.__devtoolIgnore) return false; @@ -341,36 +285,46 @@ class PixiWrapper { } private init() { - this.overlay.init(); - this.properties.init(); - this.stats.init(); - this.tree.init(); + this.scene.init(); this.textures.init(); this.rendering.init(); this._initialized = true; } private _resetState() { - this.state.setSceneGraph(null); - this.state.setStats({}); - this.state.setSelectedNode(null); - this.state.setActiveProps([]); + // TODO: this will cuase us to look through all the iframes each frame if version is not present, we need to add a flag this.state.setVersion(this.version === '' ? `>${this.majorVersion}.0.0` : this.version); - this.state.setSceneTreeData({ buttons: [] }); } private preupdate() { this._resetState(); - this.stats.preupdate(); - this.tree.preupdate(); + this.scene.preupdate(); + this.textures.preupdate(); + this.rendering.preupdate(); + } + + private _update() { + this.scene.update(); + this.textures.update(); + this.rendering.update(); + } + + private updatedThrottled() { + this.scene.throttledUpdate(); + this.textures.throttledUpdate(); + this.rendering.throttledUpdate(); + } + + private updateLoop(container: Container) { + this.scene.loop(container); + this.textures.loop(container); + this.rendering.loop(container); } private postupdate() { - this.stats.complete(); - this.tree.complete(); - this.properties.update(); - this.properties.complete(); - this.overlay.complete(); + this.scene.postupdate(); + this.textures.postupdate(); + this.rendering.postupdate(); try { // post the state to the devtools diff --git a/packages/backend/src/rendering/rendering.ts b/packages/backend/src/rendering/rendering.ts index d58fd2e..c020e18 100644 --- a/packages/backend/src/rendering/rendering.ts +++ b/packages/backend/src/rendering/rendering.ts @@ -12,6 +12,7 @@ import type { import type { FrameCaptureData, RenderingState } from '@devtool/frontend/pages/rendering/rendering'; import type { Batch, + BatcherPipe, CanvasSource, Container, GlGeometrySystem, @@ -27,10 +28,9 @@ import type { TextureSource, TilingSprite, WebGLRenderer, - BatcherPipe, WebGPURenderer, } from 'pixi.js'; -import type { PixiDevtools } from '../pixi'; +import { PixiHandler } from '../handler'; import { getPixiType } from '../utils/getPixiType'; import { loop } from '../utils/loop'; import { @@ -51,8 +51,7 @@ interface PixiMeshObjectInstruction { mesh: Mesh; } -export class Rendering { - private _devtool: typeof PixiDevtools; +export class Rendering extends PixiHandler { private _textureCache: Map = new Map(); private _glDrawFn!: GlGeometrySystem['draw']; @@ -72,11 +71,7 @@ export class Rendering { private stats = new Stats(); - constructor(devtool: typeof PixiDevtools) { - this._devtool = devtool; - } - - public reset() { + public override reset() { // restore all overriden functions const renderer = this._devtool.renderer; @@ -95,7 +90,7 @@ export class Rendering { } } - public init() { + public override init() { this._textureCache.clear(); this.stats.reset(); @@ -187,11 +182,11 @@ export class Rendering { return res; }; } - public update() { + + public override update() { this.stats.drawCalls = 0; this.stats.update(); } - public complete() {} public captureCanvasData(): RenderingState['canvasData'] { const renderer = this._devtool.renderer; diff --git a/packages/backend/src/scene/overlay/overlay.ts b/packages/backend/src/scene/overlay/overlay.ts index 63e6b6a..9f6f645 100644 --- a/packages/backend/src/scene/overlay/overlay.ts +++ b/packages/backend/src/scene/overlay/overlay.ts @@ -4,8 +4,10 @@ import { loop } from '../../utils/loop'; import type { OverlayExtension } from '@pixi/devtools'; import { getExtensionProp } from '../../extensions/getExtension'; import { extensions } from '../../extensions/Extensions'; +import { PixiHandler } from '../../handler'; +import { DevtoolMessage } from '@devtool/frontend/types'; -export class Overlay { +export class Overlay extends PixiHandler { static extensions: OverlayExtension[] = []; private _canvas!: HTMLCanvasElement; private _overlay!: HTMLDivElement; @@ -14,7 +16,6 @@ export class Overlay { private _pickerEnabled = false; private _highlightEnabled = false; private _hoveredNode: Container | null = null; - private _devtool: typeof PixiDevtools; // cache the extension for bounds as this happens on every frame private _boundsExt!: Required; @@ -22,18 +23,19 @@ export class Overlay { private _selectedStylesExt!: Required; private _hoverStylesExt!: Required; - constructor(devtool: typeof PixiDevtools) { - this._devtool = devtool; + private _keydown = false; + constructor(devtool: typeof PixiDevtools) { + super(devtool); const handleKeyDown = (e: KeyboardEvent) => { if (e.altKey) { - this.enablePicker(true); + window.postMessage({ method: DevtoolMessage.overlayStateUpdate, data: { overlayPickerEnabled: true } }, '*'); } }; const handleKeyUp = (e: KeyboardEvent) => { if (!e.altKey) { - this.enablePicker(false); + window.postMessage({ method: DevtoolMessage.overlayStateUpdate, data: { overlayPickerEnabled: false } }, '*'); } }; @@ -41,13 +43,13 @@ export class Overlay { window.addEventListener('keyup', handleKeyUp); } - public init() { + public override init() { this._boundsExt = getExtensionProp(Overlay.extensions, 'getGlobalBounds'); const newCanvas = this._devtool.canvas!; - this._highlightEnabled = this._devtool.state.overlayHighlightEnabled; - this._pickerEnabled = this._devtool.state.overlayPickerEnabled; + this._highlightEnabled = false; + this._pickerEnabled = false; if (newCanvas === this._canvas) { return; @@ -61,18 +63,13 @@ export class Overlay { this._buildHighlight('_hoverHighlight', {}); } - public update() { + public override update() { this._updateOverlay(); this.enableHighlight(this._highlightEnabled); } - public complete() { - this._devtool.state.overlayHighlightEnabled = this._highlightEnabled; - this._devtool.state.overlayPickerEnabled = this._pickerEnabled; - } - public enablePicker(value: boolean) { - this._pickerEnabled = value; + this._pickerEnabled = this._keydown ? true : value; if (this._pickerEnabled) { this.activatePick(); @@ -94,7 +91,7 @@ export class Overlay { public enableHighlight(value: boolean) { this._highlightEnabled = value; - const selectedNode = this._devtool.tree.selectedNode; + const selectedNode = this._devtool.scene.tree.selectedNode; if (!selectedNode || !this._highlightEnabled) { this.disableHighlight('_selectedHighlight'); @@ -102,7 +99,7 @@ export class Overlay { this.activateHighlight('_selectedHighlight', selectedNode); this._updateHighlight( '_selectedHighlight', - this._selectedStylesExt.getSelectedStyle(this._devtool.tree.selectedNode), + this._selectedStylesExt.getSelectedStyle(this._devtool.scene.tree.selectedNode), ); } @@ -114,7 +111,7 @@ export class Overlay { } } - public activateHighlight(type: '_selectedHighlight' | '_hoverHighlight', node: Container) { + private activateHighlight(type: '_selectedHighlight' | '_hoverHighlight', node: Container) { const bounds = this._boundsExt.getGlobalBounds(node); Object.assign(this[type].style, { transform: `translate(${bounds.x}px, ${bounds.y}px)`, @@ -123,14 +120,14 @@ export class Overlay { }); } - public disableHighlight(type: '_selectedHighlight' | '_hoverHighlight') { + private disableHighlight(type: '_selectedHighlight' | '_hoverHighlight') { this[type].style.transform = 'scale(0)'; } public highlight(id: string) { - const node = this._devtool.tree['_idMap'].get(id); + const node = this._devtool.scene.tree['_idMap'].get(id); - if (node === this._devtool.tree.selectedNode) { + if (node === this._devtool.scene.tree.selectedNode) { this._hoveredNode = null; this.disableHighlight('_hoverHighlight'); return; @@ -246,7 +243,7 @@ export class Overlay { }); if (hit) { - this._devtool.tree.setSelectedFromNode(hit); + this._devtool.scene.tree.setSelectedFromNode(hit); this.enablePicker(false); } } diff --git a/packages/backend/src/scene/scene.ts b/packages/backend/src/scene/scene.ts new file mode 100644 index 0000000..eb520b7 --- /dev/null +++ b/packages/backend/src/scene/scene.ts @@ -0,0 +1,73 @@ +import type { Container } from 'pixi.js'; +import { PixiHandler } from '../handler'; +import { Stats } from './stats/stats'; +import type { PixiDevtools } from '../pixi'; +import { Overlay } from './overlay/overlay'; +import { Tree } from './tree/tree'; +import { Properties } from './tree/properties'; + +export class Scene extends PixiHandler { + public stats: Stats; + public overlay: Overlay; + public tree: Tree; + public properties: Properties; + + // scene state + // public get stats(){ + // return this.stats.stats; + // } + + constructor(devtool: typeof PixiDevtools) { + super(devtool); + this.stats = new Stats(devtool); + this.overlay = new Overlay(devtool); + this.tree = new Tree(devtool); + this.properties = new Properties(devtool); + } + + public override init() { + this.stats.init(); + this.overlay.init(); + this.tree.init(); + this.properties.init(); + } + public override reset() { + this.stats.reset(); + this.overlay.reset(); + this.tree.reset(); + this.properties.reset(); + } + public override preupdate() { + this.stats.preupdate(); + this.overlay.preupdate(); + this.tree.preupdate(); + this.properties.preupdate(); + } + public override loop(container: Container) { + this.stats.loop(container); + this.tree.loop(container); + } + public override postupdate() { + this.stats.postupdate(); + this.overlay.postupdate(); + this.tree.postupdate(); + this.properties.postupdate(); + } + + public override throttledUpdate() { + this.stats.throttledUpdate(); + this.overlay.throttledUpdate(); + this.tree.throttledUpdate(); + this.properties.throttledUpdate(); + } + public override update() { + this.stats.update(); + this.overlay.update(); + this.tree.update(); + this.properties.update(); + } + + public getStats() { + return this.stats.stats; + } +} diff --git a/packages/backend/src/scene/stats/stats.ts b/packages/backend/src/scene/stats/stats.ts index 1c4aa6e..cc4835f 100644 --- a/packages/backend/src/scene/stats/stats.ts +++ b/packages/backend/src/scene/stats/stats.ts @@ -1,19 +1,16 @@ import type { StatsExtension } from '@pixi/devtools'; -import type { PixiDevtools } from '../../pixi'; import type { Container } from 'pixi.js'; -import { getExtensionsProp } from '../../extensions/getExtension'; import { extensions } from '../../extensions/Extensions'; +import { getExtensionsProp } from '../../extensions/getExtension'; +import { PixiHandler } from '../../handler'; -export class Stats { +export class Stats extends PixiHandler { public static extensions: StatsExtension[] = []; private _extensions: Required[] = []; - private _devtool: typeof PixiDevtools; - constructor(devtool: typeof PixiDevtools) { - this._devtool = devtool; - } + public stats: Record = {}; - public init() { + public override init() { this._extensions = getExtensionsProp(Stats.extensions, 'track'); const allKeys: string[] = []; for (const plugin of this._extensions) { @@ -28,28 +25,26 @@ export class Stats { } } - public preupdate() {} + public override reset() { + this.stats = {}; + } + + public override throttledUpdate(): void { + this.stats = {}; + } - public update(container: Container) { - const state = this._devtool.state.stats!; + public override loop(container: Container) { for (const plugin of this._extensions) { - plugin.track(container, state); + plugin.track(container, this.stats); } } - public complete() { - // remove any nodes that are at 0 - const state = this._devtool.state.stats!; - - for (const key in state) { - if (state[key] === 0) { - delete state[key]; - } - + public override postupdate() { + for (const key in this.stats) { // also format the keys to be more readable const formattedKey = this.formatCamelCase(key); - state[formattedKey] = state[key]; - delete state[key]; + this.stats[formattedKey] = this.stats[key]; + delete this.stats[key]; } } diff --git a/packages/backend/src/scene/tree/properties.ts b/packages/backend/src/scene/tree/properties.ts index 89a9477..b19b545 100644 --- a/packages/backend/src/scene/tree/properties.ts +++ b/packages/backend/src/scene/tree/properties.ts @@ -1,24 +1,18 @@ -import type { PropertyPanelData } from '@devtool/frontend/components/properties/propertyTypes'; import type { PropertiesExtension } from '@pixi/devtools'; import { extensions } from '../../extensions/Extensions'; -import type { PixiDevtools } from '../../pixi'; +import { PixiHandler } from '../../handler'; -export class Properties { +export class Properties extends PixiHandler { public static extensions: PropertiesExtension[] = []; private _extensions!: PropertiesExtension[]; - private _devtool: typeof PixiDevtools; - constructor(devtool: typeof PixiDevtools) { - this._devtool = devtool; - } - - public init() { + public override init() { this._extensions = Properties.extensions; } public setValue(prop: string, value: any) { - const selectedNode = this._devtool.tree.selectedNode; + const selectedNode = this._devtool.scene.tree.selectedNode; if (!selectedNode) return; @@ -29,13 +23,8 @@ export class Properties { }); } - public update() { - const selectedNode = this._devtool.tree.selectedNode; - if (!selectedNode) return; - } - - public complete() { - const selectedNode = this._devtool.tree.selectedNode; + public getActiveProps() { + const selectedNode = this._devtool.scene.tree.selectedNode; if (!selectedNode) return; if (selectedNode.__devtoolLocked) return; @@ -49,7 +38,8 @@ export class Properties { }, [] as ReturnType, ); - this._devtool.state.activeProps = activeProps as PropertyPanelData[]; + + return activeProps || []; } } diff --git a/packages/backend/src/scene/tree/tree.ts b/packages/backend/src/scene/tree/tree.ts index 9f2200c..c42789c 100644 --- a/packages/backend/src/scene/tree/tree.ts +++ b/packages/backend/src/scene/tree/tree.ts @@ -1,20 +1,21 @@ -import type { Container } from 'pixi.js'; -import type { PixiDevtools } from '../../pixi'; import type { SceneGraphEntry } from '@devtool/frontend/types'; import type { PixiNodeType, TreeExtension } from '@pixi/devtools'; -import { getPixiType } from '../../utils/getPixiType'; +import type { Container } from 'pixi.js'; import { extensions } from '../../extensions/Extensions'; import { getExtensionsProp } from '../../extensions/getExtension'; +import { PixiHandler } from '../../handler'; +import { getPixiType } from '../../utils/getPixiType'; +import { loop } from '../../utils/loop'; let uidMap = new WeakMap(); let uid = 0; -export class Tree { +export class Tree extends PixiHandler { public static extensions: TreeExtension[] = []; private _sceneGraph: Map = new Map(); + private _stageNode: SceneGraphEntry | null = null; private _idMap: Map = new Map(); public selectedNode: Container | null = null; - private _devtool: typeof PixiDevtools; private _metadataExtensions: Required[] = []; private _onButtonPressExtensions: Required[] = []; private _onContextMenuExtensions: Required[] = []; @@ -24,11 +25,7 @@ export class Tree { private _onSelectedExtensions: Required[] = []; private _treePanelButtons: Required[] = []; - constructor(devtool: typeof PixiDevtools) { - this._devtool = devtool; - } - - public init() { + public override init() { uidMap = new WeakMap(); uid = 0; this._metadataExtensions = getExtensionsProp(Tree.extensions, 'updateNodeMetadata'); @@ -43,6 +40,84 @@ export class Tree { this.selectedNode = null; this._idMap.clear(); this._sceneGraph.clear(); + this._stageNode = null; + } + + public getSceneGraph() { + if (!this._devtool.stage) return null; + if (this._devtool.renderer!.lastObjectRendered !== this._devtool.stage) return null; + + this.clear(); + loop({ + container: this._devtool.stage, + loop: (container) => { + this.updateLoop(container); + }, + test: (container) => { + if (container.__devtoolIgnore) return false; + if (container.__devtoolIgnoreChildren) return 'children'; + return true; + }, + }); + + // check if node has been removed, if so, clear selectedNode + if (this.selectedNode && !this._sceneGraph.has(this.selectedNode)) { + this.selectedNode = null; + window.$pixi = null; + } + + return { + sceneGraph: this._stageNode, + selectedNode: this.selectedNode ? (this._sceneGraph.get(this.selectedNode)!.id as string) : null, + sceneTreeData: { buttons: this._treePanelButtons.map((ext) => ext.panelButtons).flat() }, + }; + } + + public getSelectedNode() { + return this.selectedNode ? (this._sceneGraph.get(this.selectedNode)!.id as string) : null; + } + + private clear() { + this._sceneGraph.clear(); + this._idMap.clear(); + this._stageNode = null; + } + + private updateLoop(container: Container, updateSceneGraph = true) { + const stage = this._devtool.stage; + const type = getPixiType(container); + const { suffix, name } = this._getName(container); + const node = { + id: this._getId(container), + name: name, + children: [], + metadata: { + type, + locked: container.__devtoolLocked, + uid: this._getId(container), + suffix, + buttons: [], + contextMenu: [], + }, + } as SceneGraphEntry; + + this._metadataExtensions.forEach((ext) => { + ext.updateNodeMetadata(container, node.metadata); + }); + + this._idMap.set(node.id, container); + + if (container === stage) { + this._stageNode = node; + this._sceneGraph.set(container, node); + return; + } + + if (updateSceneGraph) { + const parent = this._sceneGraph.get(container.parent); + parent?.children.push(node); + this._sceneGraph.set(container, node); + } } public nodeButtonPress(nodeId: string, buttonAction: string, value?: boolean) { @@ -80,6 +155,7 @@ export class Tree { this.selectedNode = null; return; } + console.log('setSelected', nodeId); this.selectedNode = this._idMap.get(nodeId) ?? null; window.$pixi = this.selectedNode; this._onSelectedExtensions.forEach((ext) => { @@ -140,61 +216,6 @@ export class Tree { }); } - public preupdate() { - this._sceneGraph.clear(); - this._idMap.clear(); - } - - public complete() { - // check if node has been removed, if so, clear selectedNode - if (this.selectedNode && !this._sceneGraph.has(this.selectedNode)) { - this.selectedNode = null; - window.$pixi = null; - } - this._devtool.state.selectedNode = this.selectedNode - ? (this._sceneGraph.get(this.selectedNode)!.id as string) - : null; - - this._treePanelButtons.forEach((ext) => { - this._devtool.state.sceneTreeData!.buttons.push(...ext.panelButtons); - }); - } - - public update(container: Container) { - const stage = this._devtool.stage; - const type = getPixiType(container); - const { suffix, name } = this._getName(container); - const node = { - id: this._getId(container), - name: name, - children: [], - metadata: { - type, - locked: container.__devtoolLocked, - uid: this._getId(container), - suffix, - buttons: [], - contextMenu: [], - }, - } as SceneGraphEntry; - - this._metadataExtensions.forEach((ext) => { - ext.updateNodeMetadata(container, node.metadata); - }); - - this._idMap.set(node.id, container); - - if (container === stage) { - this._devtool.state.setSceneGraph(node); - this._sceneGraph.set(container, node); - return; - } - - const parent = this._sceneGraph.get(container.parent); - parent?.children.push(node); - this._sceneGraph.set(container, node); - } - private _getName(container: Container) { let type = container.constructor.name as PixiNodeType; diff --git a/packages/devtool-chrome/src/devtools/panel/panel.tsx b/packages/devtool-chrome/src/devtools/panel/panel.tsx index df697bf..b4b5305 100644 --- a/packages/devtool-chrome/src/devtools/panel/panel.tsx +++ b/packages/devtool-chrome/src/devtools/panel/panel.tsx @@ -14,7 +14,7 @@ const bridge: BridgeFn = (code: string) => if (err instanceof Error) { reject(err); } - reject(new Error(err.value || err.description || err.code)); + reject(new Error(err.value || err.description || err.code) + `\n${code}`); } resolve(result as any); }); diff --git a/packages/devtool-chrome/src/inject/close.ts b/packages/devtool-chrome/src/inject/close.ts index 9bd5b07..8ab75d5 100644 --- a/packages/devtool-chrome/src/inject/close.ts +++ b/packages/devtool-chrome/src/inject/close.ts @@ -1,3 +1,3 @@ -window.__PIXI_DEVTOOLS_WRAPPER__?.overlay.enableHighlight(false); -window.__PIXI_DEVTOOLS_WRAPPER__?.overlay.enablePicker(false); +window.__PIXI_DEVTOOLS_WRAPPER__?.scene.overlay.enableHighlight(false); +window.__PIXI_DEVTOOLS_WRAPPER__?.scene.overlay.enablePicker(false); window.__PIXI_DEVTOOLS_WRAPPER__?.reset(); diff --git a/packages/example/vite.config.ts b/packages/example/vite.config.ts index 290c3a9..14b545f 100644 --- a/packages/example/vite.config.ts +++ b/packages/example/vite.config.ts @@ -4,4 +4,7 @@ export default defineConfig({ build: { sourcemap: true, }, + server: { + port: 3000, + }, }); diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 6d61581..764bd40 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -9,13 +9,13 @@ import { CopyToClipboardButton } from './components/ui/clipboard'; import './globals.css'; import type { BridgeFn, NonNullableFields } from './lib/utils'; import { createSelectors, isDifferent } from './lib/utils'; +import { textureStateSelectors, textureStateSlice } from './pages/assets/assets'; import { AssetsPanel } from './pages/assets/AssetsPanel'; +import { renderingStateSelectors, renderingStateSlice } from './pages/rendering/rendering'; import { RenderingPanel } from './pages/rendering/RenderingPanel'; import { ScenePanel } from './pages/scene/ScenePanel'; -import { sceneStateSelectors, sceneStateSlice } from './pages/scene/state'; +import { sceneStateSelectors, sceneStateSlice } from './pages/scene/scene'; import type { DevtoolMessage, DevtoolState, DevtoolStateSelectors } from './types'; -import { textureStateSelectors, textureStateSlice } from './pages/assets/assets'; -import { renderingStateSelectors, renderingStateSlice } from './pages/rendering/rendering'; const tabComponents = { Scene: , @@ -72,14 +72,10 @@ const App: React.FC = ({ bridge, chromeProxy }) => { const setActive = useDevtoolStore.use.setActive(); const setChromeProxy = useDevtoolStore.use.setChromeProxy(); const setVersion = useDevtoolStore.use.setVersion(); - const setSceneGraph = useDevtoolStore.use.setSceneGraph(); const setStats = useDevtoolStore.use.setStats(); const setBridge = useDevtoolStore.use.setBridge(); const active = useDevtoolStore.use.active(); - const setSelectedNode = useDevtoolStore.use.setSelectedNode(); - const setActiveProps = useDevtoolStore.use.setActiveProps(); const setOverlayPickerEnabled = useDevtoolStore.use.setOverlayPickerEnabled(); - const setSceneTreeData = useDevtoolStore.use.setSceneTreeData(); const reset = useDevtoolStore.use.reset(); const setOverlayHighlightEnabled = useDevtoolStore.use.setOverlayHighlightEnabled(); @@ -94,7 +90,7 @@ const App: React.FC = ({ bridge, chromeProxy }) => { }); bridge('window.__PIXI_DEVTOOLS_WRAPPER__.inject()'); - bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.overlay.enableHighlight(true)`); + bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.overlay.enableHighlight(true)`); devToolsConnection.onMessage.addListener((message) => { switch (message.method as DevtoolMessage) { @@ -107,8 +103,8 @@ const App: React.FC = ({ bridge, chromeProxy }) => { { // disable the overlay picker when the panel is hidden // set previous highlight state - bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.overlay.enablePicker(false)`); - bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.overlay.enableHighlight(false)`); + bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.overlay.enablePicker(false)`); + bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.overlay.enableHighlight(false)`); } break; case 'devtool:pageReload': @@ -143,16 +139,15 @@ const App: React.FC = ({ bridge, chromeProxy }) => { isDifferent(currentState.version, data.version) && setVersion(data.version); isDifferent(currentState.stats, data.stats) && setStats(data.stats); - isDifferent(currentState.sceneGraph, data.sceneGraph) && setSceneGraph(data.sceneGraph); - isDifferent(currentState.selectedNode, data.selectedNode) && setSelectedNode(data.selectedNode); - isDifferent(currentState.activeProps, data.activeProps) && setActiveProps(data.activeProps); - isDifferent(currentState.overlayPickerEnabled, data.overlayPickerEnabled) && - setOverlayPickerEnabled(data.overlayPickerEnabled); - isDifferent(currentState.overlayHighlightEnabled, data.overlayHighlightEnabled) && - setOverlayHighlightEnabled(data.overlayHighlightEnabled); - isDifferent(currentState.sceneTreeData, data.sceneTreeData) && setSceneTreeData(data.sceneTreeData); } break; + case 'pixi-overlay-state-update': { + const data = JSON.parse(message.data) as NonNullableFields; + const currentState = useDevtoolStore.getState(); + + isDifferent(currentState.overlayPickerEnabled, data.overlayPickerEnabled) && + setOverlayPickerEnabled(data.overlayPickerEnabled); + } } }); }, [ @@ -160,14 +155,10 @@ const App: React.FC = ({ bridge, chromeProxy }) => { chromeProxy, setChromeProxy, setActive, - setActiveProps, setBridge, - setSceneGraph, - setSelectedNode, setStats, setVersion, setOverlayPickerEnabled, - setSceneTreeData, reset, setOverlayHighlightEnabled, ]); diff --git a/packages/frontend/src/components/smooth-charts/Smoothie.tsx b/packages/frontend/src/components/smooth-charts/Smoothie.tsx deleted file mode 100644 index 68d5128..0000000 --- a/packages/frontend/src/components/smooth-charts/Smoothie.tsx +++ /dev/null @@ -1,312 +0,0 @@ -import React from 'react'; -import type { IChartOptions, ITimeSeriesOptions, ITimeSeriesPresentationOptions } from 'smoothie'; -import { SmoothieChart, TimeSeries } from 'smoothie'; - -function DefaultTooltip(props: { display?: boolean; time?: number; data?: TooltipData }) { - if (!props.display) return
; - - return ( -
- {props.time} - {props.data ? ( -
    - {props.data.map((data, i) => ( -
  • {data.value}
  • - ))} -
- ) : ( -
- )} -
- ); -} - -export type ToolTip = typeof DefaultTooltip; - -// TODO: SmoothieCharts should update their types so that this is less hacky -type CanvasStyle = CanvasGradient | CanvasPattern; - -/** - * undefined means 0 - */ -type rgba = { r?: number; g?: number; b?: number; a?: number }; - -type RGBA = Required; - -export type PresentationOptions = rgba & { - fillStyle?: rgba | CanvasStyle | ITimeSeriesPresentationOptions['fillStyle']; - strokeStyle?: rgba | CanvasStyle | ITimeSeriesPresentationOptions['strokeStyle']; -} & Omit; - -function isCanvasStyle(value: any): value is CanvasStyle { - return value instanceof CanvasGradient || value instanceof CanvasPattern; -} - -function convertRGBAtoCSSString(rgba: RGBA): string { - const css = `rgba(${rgba.r},${rgba.g},${rgba.b},${rgba.a})`; - return css; -} - -function extractRGBAFromPresentationOptions(options: PresentationOptions): RGBA { - const { r, g, b, a } = options; - return { - r: r ?? 0, - g: g ?? 0, - b: b ?? 0, - a: a ?? 0, - }; -} - -/** - * We want to let users specify the presentation options a little more loosely. - * - * This converts our `PresentationOptions` to the options that SmoothieChart expects. - */ -function seriesOptsParser(opts: PresentationOptions): ITimeSeriesPresentationOptions { - const defColor = extractRGBAFromPresentationOptions(opts); - - let fillStyle: PresentationOptions['fillStyle']; - - if (isCanvasStyle(opts.fillStyle) || typeof opts.fillStyle === 'string') { - fillStyle = opts.fillStyle; - } else { - fillStyle = convertRGBAtoCSSString({ ...defColor, ...{ a: 0.2 }, ...opts.fillStyle }); - } - - let strokeStyle: PresentationOptions['strokeStyle']; - - if (isCanvasStyle(opts.strokeStyle) || typeof opts.strokeStyle === 'string') { - strokeStyle = opts.strokeStyle; - } else { - strokeStyle = convertRGBAtoCSSString({ ...defColor, ...{ a: 1 }, ...opts.strokeStyle }); - } - - const ret = { - ...opts, - data: '', - // TODO: SmoothieCharts should update their types so that this is less hacky - fillStyle: fillStyle as ITimeSeriesPresentationOptions['fillStyle'], - strokeStyle: strokeStyle as ITimeSeriesPresentationOptions['strokeStyle'], - }; - - delete ret.r; - delete ret.g; - delete ret.b; - delete ret.a; - // @ts-expect-error - third party library - delete ret.data; - - return ret; -} - -type TooltipData = { series: any; index: number; value: number }[]; - -type SmoothieComponentState = { - tooltip: { time?: number; data?: TooltipData; display?: boolean; top?: number; left?: number }; -}; - -export type SmoothieComponentSeries = { data: TimeSeries } & PresentationOptions; - -type Omit = Pick>; - -/** - * Props that we've defined in this package - */ -type ReactSmoothieProps = { - streamDelay?: number; - height?: number; - width?: number; - series?: SmoothieComponentSeries[]; - tooltip?: true | false | ToolTip; - doNotSimplifyData?: boolean; - style?: React.CSSProperties; - tooltipParentStyle?: React.CSSProperties; - containerStyle?: React.CSSProperties; - classNameCanvas?: string; - className?: string; - classNameTooltip?: string; - classNameContainer?: string; -}; - -/** - * Props that we pass onto underlying Smoothie instance - */ -type SmoothieProps = Omit; - -export type SmoothieComponentProps = ReactSmoothieProps & SmoothieProps; - -class SmoothieComponent extends React.Component { - smoothie: SmoothieChart; - canvas!: HTMLCanvasElement; - static defaultProps = { - width: 800, - height: 30, - streamDelay: 0, - }; - constructor(props: SmoothieComponentProps) { - super(props); - this.state = { tooltip: {} }; - - const opts: IChartOptions = Object.assign({}, props, { tooltip: !!props.tooltip }); - - // SmoothieCharts's tooltip injects a div at the end of the page. - // This will not do. We shall make our own and intercept the data. - - const updateTooltip = (o: SmoothieComponentState['tooltip']) => { - this.setState((state) => { - Object.assign(state.tooltip, o); - return state; - }); - }; - - opts.tooltipFormatter = (t, data) => { - updateTooltip({ - time: t, - data: props.doNotSimplifyData - ? data - : data.map((set) => ({ - index: set.index, - value: set.value, - series: { options: (set.series as TimeSeries & { options: any }).options }, - })), - }); - - return ''; - }; - - const smoothie = new SmoothieChart(opts) as SmoothieChart & { - // We need to tell TypeScript about some non-exposed internal variables - - mouseY: number; - mouseX: number; - - // TODO: type this more better - tooltipEl: any; - }; - - let lastDisplay: string; - - // Intercept the set data - smoothie.tooltipEl = { - style: { - // Intercept when smoothie.js sets tooltipEl.style.display - set display(v: 'block' | 'string') { - if (v === lastDisplay) return; - lastDisplay = v; - updateTooltip({ display: v == 'block' }); - }, - // Get smoothie's mouse events - set top(_v: any) { - updateTooltip({ - top: smoothie.mouseY, - left: smoothie.mouseX, - }); - }, - }, - }; - - if (props.series) { - props.series.forEach((series) => { - if (!(series.data instanceof TimeSeries)) { - throw Error('Invalid type passed to series option'); - } - - smoothie.addTimeSeries(series.data, seriesOptsParser(series)); - }); - } - - this.smoothie = smoothie; - } - - componentWillUnmount() { - this.smoothie.stop(); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - componentDidUpdate(prevProps: SmoothieComponentProps, _prevState: SmoothieComponentState) { - for (const series of prevProps.series!) { - if (!this.props.series!.includes(series)) this.smoothie.removeTimeSeries(series.data); - } - - for (const series of this.props.series!) { - if (!prevProps.series!.includes(series)) this.smoothie.addTimeSeries(series.data, seriesOptsParser(series)); - } - } - - render() { - let style = {} as React.CSSProperties; - - // style.maxWidth = this.props.width; - // style.maxHeight = this.props.height; - // Prevent extra pixels in wrapping element - style.display = 'block'; - - style = this.props.style || style; - - const tooltipParentStyle = this.props.tooltipParentStyle || { - pointerEvents: 'none', - position: 'absolute', - left: this.state.tooltip.left, - top: this.state.tooltip.top, - }; - - let Tooltip = this.props.tooltip as boolean | ToolTip; - - if (Tooltip === true) { - Tooltip = DefaultTooltip; - } - - const canvas = ( - { - (this.canvas = canv!) && this.smoothie.streamTo(canv, this.props.streamDelay); - const smoothieCanvas = (this.smoothie as any).canvas; - smoothieCanvas.style.width = `${this.props.width}px`; - smoothieCanvas.style.height = `${this.props.height}px`; - smoothieCanvas.width = this.props.width!; - smoothieCanvas.height = this.props.height!; - }} - /> - ); - - let tooltip; - if (Tooltip) { - tooltip = ( -
- -
- ); - } - - return ( -
- {canvas} - {tooltip} -
- ); - } - - addTimeSeries(addOpts: PresentationOptions): TimeSeries; - addTimeSeries(tsOpts: ITimeSeriesOptions, addOpts: PresentationOptions): TimeSeries; - addTimeSeries(tsOpts: PresentationOptions | ITimeSeriesOptions, addOpts?: PresentationOptions): TimeSeries { - if (addOpts === undefined) { - addOpts = tsOpts as PresentationOptions; - tsOpts = undefined as any; - } - - const ts = tsOpts instanceof TimeSeries ? tsOpts : new TimeSeries(tsOpts as ITimeSeriesOptions); - - this.smoothie.addTimeSeries(ts, seriesOptsParser(addOpts)); - return ts; - } - - removeTimeSeries(ts: TimeSeries) { - this.smoothie.removeTimeSeries(ts); - } -} - -export { DefaultTooltip, TimeSeries, SmoothieComponent as default }; diff --git a/packages/frontend/src/components/smooth-charts/vendor-smoothie.ts b/packages/frontend/src/components/smooth-charts/vendor-smoothie.ts deleted file mode 100644 index 9aefc25..0000000 --- a/packages/frontend/src/components/smooth-charts/vendor-smoothie.ts +++ /dev/null @@ -1,1148 +0,0 @@ -/* eslint-disable */ -// @ts-nocheck -// MIT License: -// -// Copyright (c) 2010-2013, Joe Walnes -// 2013-2018, Drew Noakes -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -/** - * Smoothie Charts - http://smoothiecharts.org/ - * (c) 2010-2013, Joe Walnes - * 2013-2018, Drew Noakes - * - * v1.0: Main charting library, by Joe Walnes - * v1.1: Auto scaling of axis, by Neil Dunn - * v1.2: fps (frames per second) option, by Mathias Petterson - * v1.3: Fix for divide by zero, by Paul Nikitochkin - * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds - * v1.5: Set default frames per second to 50... smoother. - * .start(), .stop() methods for conserving CPU, by Dmitry Vyal - * options.interpolation = 'bezier' or 'line', by Dmitry Vyal - * options.maxValue to fix scale, by Dmitry Vyal - * v1.6: minValue/maxValue will always get converted to floats, by Przemek Matylla - * v1.7: options.grid.fillStyle may be a transparent color, by Dmitry A. Shashkin - * Smooth rescaling, by Kostas Michalopoulos - * v1.8: Set max length to customize number of live points in the dataset with options.maxDataSetLength, by Krishna Narni - * v1.9: Display timestamps along the bottom, by Nick and Stev-io - * (https://groups.google.com/forum/?fromgroups#!topic/smoothie-charts/-Ywse8FCpKI%5B1-25%5D) - * Refactored by Krishna Narni, to support timestamp formatting function - * v1.10: Switch to requestAnimationFrame, removed the now obsoleted options.fps, by Gergely Imreh - * v1.11: options.grid.sharpLines option added, by @drewnoakes - * Addressed warning seen in Firefox when seriesOption.fillStyle undefined, by @drewnoakes - * v1.12: Support for horizontalLines added, by @drewnoakes - * Support for yRangeFunction callback added, by @drewnoakes - * v1.13: Fixed typo (#32), by @alnikitich - * v1.14: Timer cleared when last TimeSeries removed (#23), by @davidgaleano - * Fixed diagonal line on chart at start/end of data stream, by @drewnoakes - * v1.15: Support for npm package (#18), by @dominictarr - * Fixed broken removeTimeSeries function (#24) by @davidgaleano - * Minor performance and tidying, by @drewnoakes - * v1.16: Bug fix introduced in v1.14 relating to timer creation/clearance (#23), by @drewnoakes - * TimeSeries.append now deals with out-of-order timestamps, and can merge duplicates, by @zacwitte (#12) - * Documentation and some local variable renaming for clarity, by @drewnoakes - * v1.17: Allow control over font size (#10), by @drewnoakes - * Timestamp text won't overlap, by @drewnoakes - * v1.18: Allow control of max/min label precision, by @drewnoakes - * Added 'borderVisible' chart option, by @drewnoakes - * Allow drawing series with fill but no stroke (line), by @drewnoakes - * v1.19: Avoid unnecessary repaints, and fixed flicker in old browsers having multiple charts in document (#40), by @asbai - * v1.20: Add SmoothieChart.getTimeSeriesOptions and SmoothieChart.bringToFront functions, by @drewnoakes - * v1.21: Add 'step' interpolation mode, by @drewnoakes - * v1.22: Add support for different pixel ratios. Also add optional y limit formatters, by @copacetic - * v1.23: Fix bug introduced in v1.22 (#44), by @drewnoakes - * v1.24: Fix bug introduced in v1.23, re-adding parseFloat to y-axis formatter defaults, by @siggy_sf - * v1.25: Fix bug seen when adding a data point to TimeSeries which is older than the current data, by @Nking92 - * Draw time labels on top of series, by @comolosabia - * Add TimeSeries.clear function, by @drewnoakes - * v1.26: Add support for resizing on high device pixel ratio screens, by @copacetic - * v1.27: Fix bug introduced in v1.26 for non whole number devicePixelRatio values, by @zmbush - * v1.28: Add 'minValueScale' option, by @megawac - * Fix 'labelPos' for different size of 'minValueString' 'maxValueString', by @henryn - * v1.29: Support responsive sizing, by @drewnoakes - * v1.29.1: Include types in package, and make property optional, by @TrentHouliston - * v1.30: Fix inverted logic in devicePixelRatio support, by @scanlime - * v1.31: Support tooltips, by @Sly1024 and @drewnoakes - * v1.32: Support frame rate limit, by @dpuyosa - * v1.33: Use Date static method instead of instance, by @nnnoel - * Fix bug with tooltips when multiple charts on a page, by @jpmbiz70 - * v1.34: Add disabled option to TimeSeries, by @TechGuard (#91) - * Add nonRealtimeData option, by @annazhelt (#92, #93) - * Add showIntermediateLabels option, by @annazhelt (#94) - * Add displayDataFromPercentile option, by @annazhelt (#95) - * Fix bug when hiding tooltip element, by @ralphwetzel (#96) - * Support intermediate y-axis labels, by @beikeland (#99) - * v1.35: Fix issue with responsive mode at high DPI, by @drewnoakes (#101) - * v1.36: Add tooltipLabel to ITimeSeriesPresentationOptions. - * If tooltipLabel is present, tooltipLabel displays inside tooltip - * next to value, by @jackdesert (#102) - * Fix bug rendering issue in series fill when using scroll backwards, by @olssonfredrik - * Add title option, by @mesca - */ - -(function (exports) { - // Date.now polyfill - Date.now = - Date.now || - function () { - return new Date().getTime(); - }; - - var Util = { - extend: function () { - arguments[0] = arguments[0] || {}; - for (var i = 1; i < arguments.length; i++) { - for (var key in arguments[i]) { - if (arguments[i].hasOwnProperty(key)) { - if (typeof arguments[i][key] === 'object') { - if (arguments[i][key] instanceof Array) { - arguments[0][key] = arguments[i][key]; - } else { - arguments[0][key] = Util.extend(arguments[0][key], arguments[i][key]); - } - } else { - arguments[0][key] = arguments[i][key]; - } - } - } - } - return arguments[0]; - }, - binarySearch: function (data, value) { - var low = 0, - high = data.length; - while (low < high) { - var mid = (low + high) >> 1; - if (value < data[mid][0]) high = mid; - else low = mid + 1; - } - return low; - }, - }; - - /** - * Initialises a new TimeSeries with optional data options. - * - * Options are of the form (defaults shown): - * - *
-   * {
-   *   resetBounds: true,        // enables/disables automatic scaling of the y-axis
-   *   resetBoundsInterval: 3000 // the period between scaling calculations, in millis
-   * }
-   * 
- * - * Presentation options for TimeSeries are specified as an argument to SmoothieChart.addTimeSeries. - * - * @constructor - */ - function TimeSeries(options) { - this.options = Util.extend({}, TimeSeries.defaultOptions, options); - this.disabled = false; - this.clear(); - } - - TimeSeries.defaultOptions = { - resetBoundsInterval: 3000, - resetBounds: true, - }; - - /** - * Clears all data and state from this TimeSeries object. - */ - TimeSeries.prototype.clear = function () { - this.data = []; - this.maxValue = Number.NaN; // The maximum value ever seen in this TimeSeries. - this.minValue = Number.NaN; // The minimum value ever seen in this TimeSeries. - }; - - /** - * Recalculate the min/max values for this TimeSeries object. - * - * This causes the graph to scale itself in the y-axis. - */ - TimeSeries.prototype.resetBounds = function () { - if (this.data.length) { - // Walk through all data points, finding the min/max value - this.maxValue = this.data[0][1]; - this.minValue = this.data[0][1]; - for (var i = 1; i < this.data.length; i++) { - var value = this.data[i][1]; - if (value > this.maxValue) { - this.maxValue = value; - } - if (value < this.minValue) { - this.minValue = value; - } - } - } else { - // No data exists, so set min/max to NaN - this.maxValue = Number.NaN; - this.minValue = Number.NaN; - } - }; - - /** - * Adds a new data point to the TimeSeries, preserving chronological order. - * - * @param timestamp the position, in time, of this data point - * @param value the value of this data point - * @param sumRepeatedTimeStampValues if timestamp has an exact match in the series, this flag controls - * whether it is replaced, or the values summed (defaults to false.) - */ - TimeSeries.prototype.append = function (timestamp, value, sumRepeatedTimeStampValues) { - // Rewind until we hit an older timestamp - var i = this.data.length - 1; - while (i >= 0 && this.data[i][0] > timestamp) { - i--; - } - - if (i === -1) { - // This new item is the oldest data - this.data.splice(0, 0, [timestamp, value]); - } else if (this.data.length > 0 && this.data[i][0] === timestamp) { - // Update existing values in the array - if (sumRepeatedTimeStampValues) { - // Sum this value into the existing 'bucket' - this.data[i][1] += value; - value = this.data[i][1]; - } else { - // Replace the previous value - this.data[i][1] = value; - } - } else if (i < this.data.length - 1) { - // Splice into the correct position to keep timestamps in order - this.data.splice(i + 1, 0, [timestamp, value]); - } else { - // Add to the end of the array - this.data.push([timestamp, value]); - } - - this.maxValue = isNaN(this.maxValue) ? value : Math.max(this.maxValue, value); - this.minValue = isNaN(this.minValue) ? value : Math.min(this.minValue, value); - }; - - TimeSeries.prototype.dropOldData = function (oldestValidTime, maxDataSetLength) { - // We must always keep one expired data point as we need this to draw the - // line that comes into the chart from the left, but any points prior to that can be removed. - var removeCount = 0; - while (this.data.length - removeCount >= maxDataSetLength && this.data[removeCount + 1][0] < oldestValidTime) { - removeCount++; - } - if (removeCount !== 0) { - this.data.splice(0, removeCount); - } - }; - - /** - * Initialises a new SmoothieChart. - * - * Options are optional, and should be of the form below. Just specify the values you - * need and the rest will be given sensible defaults as shown: - * - *
-   * {
-   *   minValue: undefined,                      // specify to clamp the lower y-axis to a given value
-   *   maxValue: undefined,                      // specify to clamp the upper y-axis to a given value
-   *   maxValueScale: 1,                         // allows proportional padding to be added above the chart. for 10% padding, specify 1.1.
-   *   minValueScale: 1,                         // allows proportional padding to be added below the chart. for 10% padding, specify 1.1.
-   *   yRangeFunction: undefined,                // function({min: , max: }) { return {min: , max: }; }
-   *   scaleSmoothing: 0.125,                    // controls the rate at which y-value zoom animation occurs
-   *   millisPerPixel: 20,                       // sets the speed at which the chart pans by
-   *   enableDpiScaling: true,                   // support rendering at different DPI depending on the device
-   *   yMinFormatter: function(min, precision) { // callback function that formats the min y value label
-   *     return parseFloat(min).toFixed(precision);
-   *   },
-   *   yMaxFormatter: function(max, precision) { // callback function that formats the max y value label
-   *     return parseFloat(max).toFixed(precision);
-   *   },
-   *   yIntermediateFormatter: function(intermediate, precision) { // callback function that formats the intermediate y value labels
-   *     return parseFloat(intermediate).toFixed(precision);
-   *   },
-   *   maxDataSetLength: 2,
-   *   interpolation: 'bezier'                   // one of 'bezier', 'linear', or 'step'
-   *   timestampFormatter: null,                 // optional function to format time stamps for bottom of chart
-   *                                             // you may use SmoothieChart.timeFormatter, or your own: function(date) { return ''; }
-   *   scrollBackwards: false,                   // reverse the scroll direction of the chart
-   *   horizontalLines: [],                      // [ { value: 0, color: '#ffffff', lineWidth: 1 } ]
-   *   grid:
-   *   {
-   *     fillStyle: '#000000',                   // the background colour of the chart
-   *     lineWidth: 1,                           // the pixel width of grid lines
-   *     strokeStyle: '#777777',                 // colour of grid lines
-   *     millisPerLine: 1000,                    // distance between vertical grid lines
-   *     sharpLines: false,                      // controls whether grid lines are 1px sharp, or softened
-   *     verticalSections: 2,                    // number of vertical sections marked out by horizontal grid lines
-   *     borderVisible: true                     // whether the grid lines trace the border of the chart or not
-   *   },
-   *   labels
-   *   {
-   *     disabled: false,                        // enables/disables labels showing the min/max values
-   *     fillStyle: '#ffffff',                   // colour for text of labels,
-   *     fontSize: 15,
-   *     fontFamily: 'sans-serif',
-   *     precision: 2,
-   *     showIntermediateLabels: false,          // shows intermediate labels between min and max values along y axis
-   *     intermediateLabelSameAxis: true,
-   *   },
-   *   title
-   *   {
-   *     text: '',                               // the text to display on the left side of the chart
-   *     fillStyle: '#ffffff',                   // colour for text
-   *     fontSize: 15,
-   *     fontFamily: 'sans-serif',
-   *     verticalAlign: 'middle'                 // one of 'top', 'middle', or 'bottom'
-   *   },
-   *   tooltip: false                            // show tooltip when mouse is over the chart
-   *   tooltipLine: {                            // properties for a vertical line at the cursor position
-   *     lineWidth: 1,
-   *     strokeStyle: '#BBBBBB'
-   *   },
-   *   tooltipFormatter: SmoothieChart.tooltipFormatter, // formatter function for tooltip text
-   *   nonRealtimeData: false,                   // use time of latest data as current time
-   *   displayDataFromPercentile: 1,             // display not latest data, but data from the given percentile
-   *                                             // useful when trying to see old data saved by setting a high value for maxDataSetLength
-   *                                             // should be a value between 0 and 1
-   *   responsive: false,                        // whether the chart should adapt to the size of the canvas
-   *   limitFPS: 0                               // maximum frame rate the chart will render at, in FPS (zero means no limit)
-   * }
-   * 
- * - * @constructor - */ - function SmoothieChart(options) { - this.options = Util.extend({}, SmoothieChart.defaultChartOptions, options); - this.seriesSet = []; - this.currentValueRange = 1; - this.currentVisMinValue = 0; - this.lastRenderTimeMillis = 0; - this.lastChartTimestamp = 0; - - this.mousemove = this.mousemove.bind(this); - this.mouseout = this.mouseout.bind(this); - } - - /** Formats the HTML string content of the tooltip. */ - SmoothieChart.tooltipFormatter = function (timestamp, data) { - var timestampFormatter = this.options.timestampFormatter || SmoothieChart.timeFormatter, - lines = [timestampFormatter(new Date(timestamp))], - label; - - for (var i = 0; i < data.length; ++i) { - label = data[i].series.options.tooltipLabel || ''; - if (label !== '') { - label = label + ' '; - } - lines.push( - '' + - label + - this.options.yMaxFormatter(data[i].value, this.options.labels.precision) + - '', - ); - } - - return lines.join('
'); - }; - - SmoothieChart.defaultChartOptions = { - millisPerPixel: 20, - enableDpiScaling: true, - yMinFormatter: function (min, precision) { - return parseFloat(min).toFixed(precision); - }, - yMaxFormatter: function (max, precision) { - return parseFloat(max).toFixed(precision); - }, - yIntermediateFormatter: function (intermediate, precision) { - return parseFloat(intermediate).toFixed(precision); - }, - maxValueScale: 1, - minValueScale: 1, - interpolation: 'bezier', - scaleSmoothing: 0.125, - maxDataSetLength: 2, - scrollBackwards: false, - displayDataFromPercentile: 1, - grid: { - fillStyle: '#000000', - strokeStyle: '#777777', - lineWidth: 1, - sharpLines: false, - millisPerLine: 1000, - verticalSections: 2, - borderVisible: true, - }, - labels: { - fillStyle: '#ffffff', - disabled: false, - fontSize: 10, - fontFamily: 'monospace', - precision: 2, - showIntermediateLabels: false, - intermediateLabelSameAxis: true, - }, - title: { - text: '', - fillStyle: '#ffffff', - fontSize: 15, - fontFamily: 'monospace', - verticalAlign: 'middle', - }, - horizontalLines: [], - tooltip: false, - tooltipLine: { - lineWidth: 1, - strokeStyle: '#BBBBBB', - }, - tooltipFormatter: SmoothieChart.tooltipFormatter, - nonRealtimeData: false, - responsive: false, - limitFPS: 0, - }; - - // Based on http://inspirit.github.com/jsfeat/js/compatibility.js - SmoothieChart.AnimateCompatibility = (function () { - var requestAnimationFrame = function (callback, element) { - var requestAnimationFrame = - window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function (callback) { - return window.setTimeout(function () { - callback(Date.now()); - }, 16); - }; - return requestAnimationFrame.call(window, callback, element); - }, - cancelAnimationFrame = function (id) { - var cancelAnimationFrame = - window.cancelAnimationFrame || - function (id) { - clearTimeout(id); - }; - return cancelAnimationFrame.call(window, id); - }; - - return { - requestAnimationFrame: requestAnimationFrame, - cancelAnimationFrame: cancelAnimationFrame, - }; - })(); - - SmoothieChart.defaultSeriesPresentationOptions = { - lineWidth: 1, - strokeStyle: '#ffffff', - }; - - /** - * Adds a TimeSeries to this chart, with optional presentation options. - * - * Presentation options should be of the form (defaults shown): - * - *
-   * {
-   *   lineWidth: 1,
-   *   strokeStyle: '#ffffff',
-   *   fillStyle: undefined,
-   *   tooltipLabel: undefined
-   * }
-   * 
- */ - SmoothieChart.prototype.addTimeSeries = function (timeSeries, options) { - this.seriesSet.push({ - timeSeries: timeSeries, - options: Util.extend({}, SmoothieChart.defaultSeriesPresentationOptions, options), - }); - if (timeSeries.options.resetBounds && timeSeries.options.resetBoundsInterval > 0) { - timeSeries.resetBoundsTimerId = setInterval(function () { - timeSeries.resetBounds(); - }, timeSeries.options.resetBoundsInterval); - } - }; - - /** - * Removes the specified TimeSeries from the chart. - */ - SmoothieChart.prototype.removeTimeSeries = function (timeSeries) { - // Find the correct timeseries to remove, and remove it - var numSeries = this.seriesSet.length; - for (var i = 0; i < numSeries; i++) { - if (this.seriesSet[i].timeSeries === timeSeries) { - this.seriesSet.splice(i, 1); - break; - } - } - // If a timer was operating for that timeseries, remove it - if (timeSeries.resetBoundsTimerId) { - // Stop resetting the bounds, if we were - clearInterval(timeSeries.resetBoundsTimerId); - } - }; - - /** - * Gets render options for the specified TimeSeries. - * - * As you may use a single TimeSeries in multiple charts with different formatting in each usage, - * these settings are stored in the chart. - */ - SmoothieChart.prototype.getTimeSeriesOptions = function (timeSeries) { - // Find the correct timeseries to remove, and remove it - var numSeries = this.seriesSet.length; - for (var i = 0; i < numSeries; i++) { - if (this.seriesSet[i].timeSeries === timeSeries) { - return this.seriesSet[i].options; - } - } - }; - - /** - * Brings the specified TimeSeries to the top of the chart. It will be rendered last. - */ - SmoothieChart.prototype.bringToFront = function (timeSeries) { - // Find the correct timeseries to remove, and remove it - var numSeries = this.seriesSet.length; - for (var i = 0; i < numSeries; i++) { - if (this.seriesSet[i].timeSeries === timeSeries) { - var set = this.seriesSet.splice(i, 1); - this.seriesSet.push(set[0]); - break; - } - } - }; - - /** - * Instructs the SmoothieChart to start rendering to the provided canvas, with specified delay. - * - * @param canvas the target canvas element - * @param delayMillis an amount of time to wait before a data point is shown. This can prevent the end of the series - * from appearing on screen, with new values flashing into view, at the expense of some latency. - */ - SmoothieChart.prototype.streamTo = function (canvas, delayMillis) { - this.canvas = canvas; - this.delay = delayMillis; - this.start(); - }; - - SmoothieChart.prototype.getTooltipEl = function () { - // Create the tool tip element lazily - if (!this.tooltipEl) { - this.tooltipEl = document.createElement('div'); - this.tooltipEl.className = 'smoothie-chart-tooltip'; - this.tooltipEl.style.position = 'absolute'; - this.tooltipEl.style.display = 'none'; - document.body.appendChild(this.tooltipEl); - } - return this.tooltipEl; - }; - - SmoothieChart.prototype.updateTooltip = function () { - if (!this.options.tooltip) { - return; - } - var el = this.getTooltipEl(); - - if (!this.mouseover || !this.options.tooltip) { - el.style.display = 'none'; - return; - } - - var time = this.lastChartTimestamp; - - // x pixel to time - var t = this.options.scrollBackwards - ? time - this.mouseX * this.options.millisPerPixel - : time - (this.canvas.offsetWidth - this.mouseX) * this.options.millisPerPixel; - - var data = []; - - // For each data set... - for (var d = 0; d < this.seriesSet.length; d++) { - var timeSeries = this.seriesSet[d].timeSeries; - if (timeSeries.disabled) { - continue; - } - - // find datapoint closest to time 't' - var closeIdx = Util.binarySearch(timeSeries.data, t); - if (closeIdx > 0 && closeIdx < timeSeries.data.length) { - data.push({ series: this.seriesSet[d], index: closeIdx, value: timeSeries.data[closeIdx][1] }); - } - } - - if (data.length) { - el.innerHTML = this.options.tooltipFormatter.call(this, t, data); - el.style.display = 'block'; - } else { - el.style.display = 'none'; - } - }; - - SmoothieChart.prototype.mousemove = function (evt) { - this.mouseover = true; - this.mouseX = evt.offsetX; - this.mouseY = evt.offsetY; - this.mousePageX = evt.pageX; - this.mousePageY = evt.pageY; - if (!this.options.tooltip) { - return; - } - var el = this.getTooltipEl(); - el.style.top = Math.round(this.mousePageY) + 'px'; - el.style.left = Math.round(this.mousePageX) + 'px'; - this.updateTooltip(); - }; - - SmoothieChart.prototype.mouseout = function () { - this.mouseover = false; - this.mouseX = this.mouseY = -1; - if (this.tooltipEl) this.tooltipEl.style.display = 'none'; - }; - - /** - * Make sure the canvas has the optimal resolution for the device's pixel ratio. - */ - SmoothieChart.prototype.resize = function () { - var dpr = !this.options.enableDpiScaling || !window ? 1 : window.devicePixelRatio, - width, - height; - if (this.options.responsive) { - // Newer behaviour: Use the canvas's size in the layout, and set the internal - // resolution according to that size and the device pixel ratio (eg: high DPI) - width = this.canvas.offsetWidth; - height = this.canvas.offsetHeight; - - if (width !== this.lastWidth) { - this.lastWidth = width; - this.canvas.setAttribute('width', Math.floor(width * dpr).toString()); - this.canvas.getContext('2d').scale(dpr, dpr); - } - if (height !== this.lastHeight) { - this.lastHeight = height; - this.canvas.setAttribute('height', Math.floor(height * dpr).toString()); - this.canvas.getContext('2d').scale(dpr, dpr); - } - } else if (dpr !== 1) { - // Older behaviour: use the canvas's inner dimensions and scale the element's size - // according to that size and the device pixel ratio (eg: high DPI) - width = parseInt(this.canvas.getAttribute('width')); - height = parseInt(this.canvas.getAttribute('height')); - - if (!this.originalWidth || Math.floor(this.originalWidth * dpr) !== width) { - this.originalWidth = width; - this.canvas.setAttribute('width', Math.floor(width * dpr).toString()); - this.canvas.style.width = width + 'px'; - this.canvas.getContext('2d').scale(dpr, dpr); - } - - if (!this.originalHeight || Math.floor(this.originalHeight * dpr) !== height) { - this.originalHeight = height; - this.canvas.setAttribute('height', Math.floor(height * dpr).toString()); - this.canvas.style.height = height + 'px'; - this.canvas.getContext('2d').scale(dpr, dpr); - } - } - }; - - /** - * Starts the animation of this chart. - */ - SmoothieChart.prototype.start = function () { - if (this.frame) { - // We're already running, so just return - return; - } - - this.canvas.addEventListener('mousemove', this.mousemove); - this.canvas.addEventListener('mouseout', this.mouseout); - - // Renders a frame, and queues the next frame for later rendering - var animate = function () { - this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame( - function () { - if (this.options.nonRealtimeData) { - var dateZero = new Date(0); - // find the data point with the latest timestamp - var maxTimeStamp = this.seriesSet.reduce( - function (max, series) { - var dataSet = series.timeSeries.data; - var indexToCheck = Math.round(this.options.displayDataFromPercentile * dataSet.length) - 1; - indexToCheck = indexToCheck >= 0 ? indexToCheck : 0; - indexToCheck = indexToCheck <= dataSet.length - 1 ? indexToCheck : dataSet.length - 1; - if (dataSet && dataSet.length > 0) { - // timestamp corresponds to element 0 of the data point - var lastDataTimeStamp = dataSet[indexToCheck][0]; - max = max > lastDataTimeStamp ? max : lastDataTimeStamp; - } - return max; - }.bind(this), - dateZero, - ); - // use the max timestamp as current time - this.render(this.canvas, maxTimeStamp > dateZero ? maxTimeStamp : null); - } else { - this.render(); - } - animate(); - }.bind(this), - ); - }.bind(this); - - animate(); - }; - - /** - * Stops the animation of this chart. - */ - SmoothieChart.prototype.stop = function () { - if (this.frame) { - SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame); - delete this.frame; - this.canvas.removeEventListener('mousemove', this.mousemove); - this.canvas.removeEventListener('mouseout', this.mouseout); - } - }; - - SmoothieChart.prototype.updateValueRange = function () { - // Calculate the current scale of the chart, from all time series. - var chartOptions = this.options, - chartMaxValue = Number.NaN, - chartMinValue = Number.NaN; - - for (var d = 0; d < this.seriesSet.length; d++) { - // TODO(ndunn): We could calculate / track these values as they stream in. - var timeSeries = this.seriesSet[d].timeSeries; - if (timeSeries.disabled) { - continue; - } - - if (!isNaN(timeSeries.maxValue)) { - chartMaxValue = !isNaN(chartMaxValue) ? Math.max(chartMaxValue, timeSeries.maxValue) : timeSeries.maxValue; - } - - if (!isNaN(timeSeries.minValue)) { - chartMinValue = !isNaN(chartMinValue) ? Math.min(chartMinValue, timeSeries.minValue) : timeSeries.minValue; - } - } - - // Scale the chartMaxValue to add padding at the top if required - if (chartOptions.maxValue != null) { - chartMaxValue = chartOptions.maxValue; - } else { - chartMaxValue *= chartOptions.maxValueScale; - } - - // Set the minimum if we've specified one - if (chartOptions.minValue != null) { - chartMinValue = chartOptions.minValue; - } else { - chartMinValue -= Math.abs(chartMinValue * chartOptions.minValueScale - chartMinValue); - } - - // If a custom range function is set, call it - if (this.options.yRangeFunction) { - var range = this.options.yRangeFunction({ min: chartMinValue, max: chartMaxValue }); - chartMinValue = range.min; - chartMaxValue = range.max; - } - - if (!isNaN(chartMaxValue) && !isNaN(chartMinValue)) { - if (chartMinValue === chartMaxValue) { - chartMinValue -= 1; - //chartMaxValue += 1; - } - - var targetValueRange = chartMaxValue - chartMinValue; - var valueRangeDiff = targetValueRange - this.currentValueRange; - var minValueDiff = chartMinValue - this.currentVisMinValue; - this.isAnimatingScale = Math.abs(valueRangeDiff) > 0.1 || Math.abs(minValueDiff) > 0.1; - const t = 0.000001; - if (Math.abs(valueRangeDiff) < t) { - valueRangeDiff = t; - } - this.currentValueRange += chartOptions.scaleSmoothing * valueRangeDiff; - this.currentVisMinValue += chartOptions.scaleSmoothing * minValueDiff; - } - - this.valueRange = { min: chartMinValue, max: chartMaxValue }; - }; - - SmoothieChart.prototype.render = function (canvas, time) { - var nowMillis = Date.now(); - - // Respect any frame rate limit. - if (this.options.limitFPS > 0 && nowMillis - this.lastRenderTimeMillis < 1000 / this.options.limitFPS) return; - - if (!this.isAnimatingScale) { - // We're not animating. We can use the last render time and the scroll speed to work out whether - // we actually need to paint anything yet. If not, we can return immediately. - - // Render at least every 1/6th of a second. The canvas may be resized, which there is - // no reliable way to detect. - var maxIdleMillis = Math.min(1000 / 6, this.options.millisPerPixel); - - if (nowMillis - this.lastRenderTimeMillis < maxIdleMillis) { - return; - } - } - - this.resize(); - this.updateTooltip(); - - this.lastRenderTimeMillis = nowMillis; - - canvas = canvas || this.canvas; - time = time || nowMillis - (this.delay || 0); - - // Round time down to pixel granularity, so motion appears smoother. - time -= time % this.options.millisPerPixel; - - this.lastChartTimestamp = time; - - var context = canvas.getContext('2d'), - chartOptions = this.options, - dimensions = { top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight }, - // Calculate the threshold time for the oldest data points. - oldestValidTime = time - dimensions.width * chartOptions.millisPerPixel, - valueToYPixel = function (value) { - var offset = value - this.currentVisMinValue; - return this.currentValueRange === 0 - ? dimensions.height - : dimensions.height - Math.round((offset / this.currentValueRange) * dimensions.height); - }.bind(this), - timeToXPixel = function (t) { - if (chartOptions.scrollBackwards) { - return Math.round((time - t) / chartOptions.millisPerPixel); - } - return Math.round(dimensions.width - (time - t) / chartOptions.millisPerPixel); - }; - - this.updateValueRange(); - - context.font = chartOptions.labels.fontSize + 'px ' + chartOptions.labels.fontFamily; - - // Save the state of the canvas context, any transformations applied in this method - // will get removed from the stack at the end of this method when .restore() is called. - context.save(); - - // Move the origin. - context.translate(dimensions.left, dimensions.top); - - // Create a clipped rectangle - anything we draw will be constrained to this rectangle. - // This prevents the occasional pixels from curves near the edges overrunning and creating - // screen cheese (that phrase should need no explanation). - context.beginPath(); - context.rect(0, 0, dimensions.width, dimensions.height); - context.clip(); - - // Clear the working area. - context.save(); - context.fillStyle = chartOptions.grid.fillStyle; - context.clearRect(0, 0, dimensions.width, dimensions.height); - context.fillRect(0, 0, dimensions.width, dimensions.height); - context.restore(); - - // Grid lines... - context.save(); - context.lineWidth = chartOptions.grid.lineWidth; - context.strokeStyle = chartOptions.grid.strokeStyle; - // Vertical (time) dividers. - if (chartOptions.grid.millisPerLine > 0) { - context.beginPath(); - for ( - var t = time - (time % chartOptions.grid.millisPerLine); - t >= oldestValidTime; - t -= chartOptions.grid.millisPerLine - ) { - var gx = timeToXPixel(t); - if (chartOptions.grid.sharpLines) { - gx -= 0.5; - } - context.moveTo(gx, 0); - context.lineTo(gx, dimensions.height); - } - context.stroke(); - context.closePath(); - } - - // Horizontal (value) dividers. - for (var v = 1; v < chartOptions.grid.verticalSections; v++) { - var gy = Math.round((v * dimensions.height) / chartOptions.grid.verticalSections); - if (chartOptions.grid.sharpLines) { - gy -= 0.5; - } - context.beginPath(); - context.moveTo(0, gy); - context.lineTo(dimensions.width, gy); - context.stroke(); - context.closePath(); - } - // Bounding rectangle. - if (chartOptions.grid.borderVisible) { - context.beginPath(); - context.strokeRect(0, 0, dimensions.width, dimensions.height); - context.closePath(); - } - context.restore(); - - // Draw any horizontal lines... - if (chartOptions.horizontalLines && chartOptions.horizontalLines.length) { - for (var hl = 0; hl < chartOptions.horizontalLines.length; hl++) { - var line = chartOptions.horizontalLines[hl], - hly = Math.round(valueToYPixel(line.value)) - 0.5; - context.strokeStyle = line.color || '#ffffff'; - context.lineWidth = line.lineWidth || 1; - context.beginPath(); - context.moveTo(0, hly); - context.lineTo(dimensions.width, hly); - context.stroke(); - context.closePath(); - } - } - - // For each data set... - for (var d = 0; d < this.seriesSet.length; d++) { - context.save(); - var timeSeries = this.seriesSet[d].timeSeries; - if (timeSeries.disabled) { - continue; - } - - var dataSet = timeSeries.data, - seriesOptions = this.seriesSet[d].options; - - // Delete old data that's moved off the left of the chart. - timeSeries.dropOldData(oldestValidTime, chartOptions.maxDataSetLength); - - // Set style for this dataSet. - context.lineWidth = seriesOptions.lineWidth; - context.strokeStyle = seriesOptions.strokeStyle; - // Draw the line... - context.beginPath(); - // Retain lastX, lastY for calculating the control points of bezier curves. - var firstX = 0, - firstY = 0, - lastX = 0, - lastY = 0; - for (var i = 0; i < dataSet.length && dataSet.length !== 1; i++) { - var x = timeToXPixel(dataSet[i][0]), - y = valueToYPixel(dataSet[i][1]); - - if (i === 0) { - firstX = x; - firstY = y; - context.moveTo(x, y); - } else { - switch (chartOptions.interpolation) { - case 'linear': - case 'line': { - context.lineTo(x, y); - break; - } - case 'bezier': - default: { - // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves - // - // Assuming A was the last point in the line plotted and B is the new point, - // we draw a curve with control points P and Q as below. - // - // A---P - // | - // | - // | - // Q---B - // - // Importantly, A and P are at the same y coordinate, as are B and Q. This is - // so adjacent curves appear to flow as one. - // - context.bezierCurveTo( - // startPoint (A) is implicit from last iteration of loop - Math.round((lastX + x) / 2), - lastY, // controlPoint1 (P) - Math.round(lastX + x) / 2, - y, // controlPoint2 (Q) - x, - y, - ); // endPoint (B) - break; - } - case 'step': { - context.lineTo(x, lastY); - context.lineTo(x, y); - break; - } - } - } - - lastX = x; - lastY = y; - } - - if (dataSet.length > 1) { - if (seriesOptions.fillStyle) { - // Close up the fill region. - if (chartOptions.scrollBackwards) { - context.lineTo(lastX, dimensions.height + seriesOptions.lineWidth); - context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth); - context.lineTo(firstX, firstY); - } else { - context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY); - context.lineTo( - dimensions.width + seriesOptions.lineWidth + 1, - dimensions.height + seriesOptions.lineWidth + 1, - ); - context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth); - } - context.fillStyle = seriesOptions.fillStyle; - context.fill(); - } - - if (seriesOptions.strokeStyle && seriesOptions.strokeStyle !== 'none') { - context.stroke(); - } - context.closePath(); - } - context.restore(); - } - - if (chartOptions.tooltip && this.mouseX >= 0) { - // Draw vertical bar to show tooltip position - context.lineWidth = chartOptions.tooltipLine.lineWidth; - context.strokeStyle = chartOptions.tooltipLine.strokeStyle; - context.beginPath(); - context.moveTo(this.mouseX, 0); - context.lineTo(this.mouseX, dimensions.height); - context.closePath(); - context.stroke(); - this.updateTooltip(); - } - - // Draw the axis values on the chart. - if (!chartOptions.labels.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) { - var maxValueString = chartOptions.yMaxFormatter(this.valueRange.max, chartOptions.labels.precision), - minValueString = chartOptions.yMinFormatter(this.valueRange.min, chartOptions.labels.precision), - maxLabelPos = chartOptions.scrollBackwards - ? 0 - : dimensions.width - context.measureText(maxValueString).width - 2, - minLabelPos = chartOptions.scrollBackwards - ? 0 - : dimensions.width - context.measureText(minValueString).width - 2; - context.fillStyle = chartOptions.labels.fillStyle; - context.fillText(maxValueString, maxLabelPos, chartOptions.labels.fontSize); - context.fillText(minValueString, minLabelPos, dimensions.height - 2); - } - - // Display intermediate y axis labels along y-axis to the left of the chart - if ( - chartOptions.labels.showIntermediateLabels && - !isNaN(this.valueRange.min) && - !isNaN(this.valueRange.max) && - chartOptions.grid.verticalSections > 0 - ) { - // show a label above every vertical section divider - var step = (this.valueRange.max - this.valueRange.min) / chartOptions.grid.verticalSections; - var stepPixels = dimensions.height / chartOptions.grid.verticalSections; - for (var v = 1; v < chartOptions.grid.verticalSections; v++) { - var gy = dimensions.height - Math.round(v * stepPixels); - if (chartOptions.grid.sharpLines) { - gy -= 0.5; - } - var yValue = chartOptions.yIntermediateFormatter(this.valueRange.min + v * step, chartOptions.labels.precision); - //left of right axis? - let intermediateLabelPos = chartOptions.labels.intermediateLabelSameAxis - ? chartOptions.scrollBackwards - ? 0 - : dimensions.width - context.measureText(yValue).width - 2 - : chartOptions.scrollBackwards - ? dimensions.width - context.measureText(yValue).width - 2 - : 0; - - context.fillText(yValue, intermediateLabelPos, gy - chartOptions.grid.lineWidth); - } - } - - // Display timestamps along x-axis at the bottom of the chart. - if (chartOptions.timestampFormatter && chartOptions.grid.millisPerLine > 0) { - var textUntilX = chartOptions.scrollBackwards - ? context.measureText(minValueString).width - : dimensions.width - context.measureText(minValueString).width + 4; - for ( - var t = time - (time % chartOptions.grid.millisPerLine); - t >= oldestValidTime; - t -= chartOptions.grid.millisPerLine - ) { - var gx = timeToXPixel(t); - // Only draw the timestamp if it won't overlap with the previously drawn one. - if ((!chartOptions.scrollBackwards && gx < textUntilX) || (chartOptions.scrollBackwards && gx > textUntilX)) { - // Formats the timestamp based on user specified formatting function - // SmoothieChart.timeFormatter function above is one such formatting option - var tx = new Date(t), - ts = chartOptions.timestampFormatter(tx), - tsWidth = context.measureText(ts).width; - - textUntilX = chartOptions.scrollBackwards ? gx + tsWidth + 2 : gx - tsWidth - 2; - - context.fillStyle = chartOptions.labels.fillStyle; - if (chartOptions.scrollBackwards) { - context.fillText(ts, gx, dimensions.height - 2); - } else { - context.fillText(ts, gx - tsWidth, dimensions.height - 2); - } - } - } - } - - // Display title. - if (chartOptions.title.text !== '') { - context.font = chartOptions.title.fontSize + 'px ' + chartOptions.title.fontFamily; - var titleXPos = chartOptions.scrollBackwards - ? dimensions.width - context.measureText(chartOptions.title.text).width - 2 - : 2; - if (chartOptions.title.verticalAlign == 'bottom') { - context.textBaseline = 'bottom'; - var titleYPos = dimensions.height; - } else if (chartOptions.title.verticalAlign == 'middle') { - context.textBaseline = 'middle'; - var titleYPos = dimensions.height / 2; - } else { - context.textBaseline = 'top'; - var titleYPos = 0; - } - context.fillStyle = chartOptions.title.fillStyle; - context.fillText(chartOptions.title.text, titleXPos, titleYPos); - } - - context.restore(); // See .save() above. - }; - - // Sample timestamp formatting function - SmoothieChart.timeFormatter = function (date) { - function pad2(number) { - return (number < 10 ? '0' : '') + number; - } - return pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds()); - }; - - exports.TimeSeries = TimeSeries; - exports.SmoothieChart = SmoothieChart; -})(typeof exports === 'undefined' ? this : exports); diff --git a/packages/frontend/src/pages/assets/gpuTextures/TextureStats.tsx b/packages/frontend/src/pages/assets/gpuTextures/TextureStats.tsx index 4dfc927..6094392 100644 --- a/packages/frontend/src/pages/assets/gpuTextures/TextureStats.tsx +++ b/packages/frontend/src/pages/assets/gpuTextures/TextureStats.tsx @@ -45,28 +45,6 @@ export const TextureStats: React.FC = () => {
); })} - {/* {currentStats.map((item) => { - return ( -
- - {item.name} ({stats![item.name as keyof typeof stats]}) - - -
- ); - })} */}
diff --git a/packages/frontend/src/pages/scene/ScenePanel.tsx b/packages/frontend/src/pages/scene/ScenePanel.tsx index 9e76218..3bd1d25 100644 --- a/packages/frontend/src/pages/scene/ScenePanel.tsx +++ b/packages/frontend/src/pages/scene/ScenePanel.tsx @@ -1,14 +1,98 @@ +import { useEffect } from 'react'; +import { useDevtoolStore } from '../../App'; import { CollapsibleSplit } from '../../components/collapsible/collapsible-split'; -import { Stats } from './stats-section/Stats'; -import { Properties } from './scene-section/Properties'; -import { SceneTree } from './scene-section/Tree'; +import { useInterval } from '../../lib/interval'; +import { isDifferent } from '../../lib/utils'; +import { SceneTree } from './graph/SceneTree'; +import { SceneStats } from './SceneStats'; +import { SceneProperties } from './graph/SceneProperties'; export const ScenePanel = () => { - // show the stats and the scene tree + const bridge = useDevtoolStore.use.bridge()!; + const savedStats = useDevtoolStore.use.stats(); + const setStats = useDevtoolStore.use.setStats(); + useInterval(async () => { + const res = await bridge>(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.getStats()`); + + if (isDifferent(res, savedStats)) { + setStats(res); + } + }, 1000); + + const savedSceneGraph = useDevtoolStore.use.sceneGraph(); + const setSceneGraph = useDevtoolStore.use.setSceneGraph(); + const savedSelectedNode = useDevtoolStore.use.selectedNode(); + const setSelectedNode = useDevtoolStore.use.setSelectedNode(); + const savedSceneTreeData = useDevtoolStore.use.sceneTreeData(); + const setSceneTreeData = useDevtoolStore.use.setSceneTreeData(); + useInterval(async () => { + const res = await bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.tree.getSceneGraph()`); + + if (!res) return; + + const { sceneGraph, selectedNode, sceneTreeData } = res as { + sceneGraph: typeof savedSceneGraph; + selectedNode: typeof savedSelectedNode; + sceneTreeData: typeof savedSceneTreeData; + }; + + if (sceneGraph !== null && isDifferent(sceneGraph, savedSceneGraph)) { + setSceneGraph(sceneGraph); + } + + if (isDifferent(selectedNode, savedSelectedNode)) { + setSelectedNode(selectedNode); + } + + if (isDifferent(sceneTreeData, savedSceneTreeData)) { + setSceneTreeData(sceneTreeData); + } + }, 1000); + + const overlayHighlightEnabled = useDevtoolStore.use.overlayHighlightEnabled(); + const overlayPickerEnabled = useDevtoolStore.use.overlayPickerEnabled(); + useInterval(async () => { + const res = await bridge(`window.__PIXI_DEVTOOLS_WRAPPER?__.scene.tree.getSelectedNode()`); + + if (isDifferent(res, savedSelectedNode)) { + setSelectedNode(res); + } + + bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.overlay.enableHighlight(${overlayHighlightEnabled})`); + bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.overlay.enablePicker(${overlayPickerEnabled})`); + }, 100); + + // send heartbeat to the backend + useEffect(() => { + bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.overlay.enableHighlight(${overlayHighlightEnabled})`); + bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.overlay.enablePicker(${overlayPickerEnabled})`); + + return () => { + bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.overlay.enableHighlight(false)`); + bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.overlay.enablePicker(false)`); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const savedActiveProps = useDevtoolStore.use.activeProps(); + const setActiveProps = useDevtoolStore.use.setActiveProps(); + + useInterval(async () => { + const res = await bridge( + `window.__PIXI_DEVTOOLS_WRAPPER__?.scene.properties.getActiveProps()`, + ); + + if (!res) return; + + if (isDifferent(res, savedActiveProps)) { + setActiveProps(res); + } + }, 100); + return (
- - } right={} rightOptions={{ minSize: 30 }} /> + + } right={} rightOptions={{ minSize: 30 }} title="Scene" />
); }; diff --git a/packages/frontend/src/pages/scene/SceneStats.tsx b/packages/frontend/src/pages/scene/SceneStats.tsx new file mode 100644 index 0000000..c752046 --- /dev/null +++ b/packages/frontend/src/pages/scene/SceneStats.tsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; +import { useDevtoolStore } from '../../App'; +import { CollapsibleSection } from '../../components/collapsible/collapsible-section'; +import { CanvasStatComponent } from '../../components/smooth-charts/stat'; +import { useTheme } from '../../components/theme-provider'; + +export const SceneStats: React.FC = () => { + const { theme } = useTheme(); + const [stats, setStats] = useState>({}); + const savedStats = useDevtoolStore.use.stats(); + + useEffect(() => { + const updatedStats = { ...stats, ...savedStats }; + setStats(updatedStats); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [savedStats]); + + return ( + +
+
+ {stats && + Object.keys(stats!).map((key) => { + const item = stats![key as keyof typeof stats]; + return ( +
+ +
+ ); + })} +
+
+
+ ); +}; diff --git a/packages/frontend/src/pages/scene/scene-section/Properties.tsx b/packages/frontend/src/pages/scene/graph/SceneProperties.tsx similarity index 97% rename from packages/frontend/src/pages/scene/scene-section/Properties.tsx rename to packages/frontend/src/pages/scene/graph/SceneProperties.tsx index 965f95d..14cfc8e 100644 --- a/packages/frontend/src/pages/scene/scene-section/Properties.tsx +++ b/packages/frontend/src/pages/scene/graph/SceneProperties.tsx @@ -87,7 +87,7 @@ function createFuse(activeProps: PropertyPanelData[]) { }); } -export const Properties: React.FC = () => { +export const SceneProperties: React.FC = () => { const activeProps = useDevtoolStore((state) => state.activeProps); const bridge = useDevtoolStore.use.bridge()!; const [currentSearch, setCurrentSearch] = useState(''); @@ -107,7 +107,7 @@ export const Properties: React.FC = () => { const handlePropertyChange = useCallback( (property: string, newValue: any) => { bridge(` - window.__PIXI_DEVTOOLS_WRAPPER__.properties.setValue('${property}', ${newValue}) + window.__PIXI_DEVTOOLS_WRAPPER__.scene.properties.setValue('${property}', ${newValue}) `); }, [bridge], diff --git a/packages/frontend/src/pages/scene/scene-section/Tree.tsx b/packages/frontend/src/pages/scene/graph/SceneTree.tsx similarity index 90% rename from packages/frontend/src/pages/scene/scene-section/Tree.tsx rename to packages/frontend/src/pages/scene/graph/SceneTree.tsx index 15b22b6..0d0d161 100644 --- a/packages/frontend/src/pages/scene/scene-section/Tree.tsx +++ b/packages/frontend/src/pages/scene/graph/SceneTree.tsx @@ -21,14 +21,16 @@ interface PanelProps { const Panel: React.FC = ({ children, onSearch, panelButtons }) => { const [searchTerm, setSearchTerm] = useState(''); const overlayPickerEnabled = useDevtoolStore.use.overlayPickerEnabled(); + const setOverlayPickerEnabled = useDevtoolStore.use.setOverlayPickerEnabled(); const overlayHighlightEnabled = useDevtoolStore.use.overlayHighlightEnabled(); + const setOverlayHighlightEnabled = useDevtoolStore.use.setOverlayHighlightEnabled(); const bridge = useDevtoolStore.use.bridge()!; const onPickerToggle = (enabled: boolean) => { - bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.overlay.enablePicker(${enabled})`); + setOverlayPickerEnabled(enabled); }; const onHighlightToggle = (enabled: boolean) => { - bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.overlay.enableHighlight(${enabled})`); + setOverlayHighlightEnabled(enabled); }; const handleSearch = (e: React.ChangeEvent) => { setSearchTerm(e.target.value); @@ -87,7 +89,7 @@ const Panel: React.FC = ({ children, onSearch, panelButtons }) => { button={button} className="text-overflow-ellipsis overflow-hidden whitespace-nowrap" onClick={() => - bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.tree.treePanelButtonPress(${JSON.stringify(button)})`) + bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?scene.tree.treePanelButtonPress(${JSON.stringify(button)})`) } /> ))} @@ -115,10 +117,10 @@ const Panel: React.FC = ({ children, onSearch, panelButtons }) => { export const SceneTree: React.FC = () => { const bridge = useDevtoolStore.use.bridge()!; - const panelButtons = useDevtoolStore.use.sceneTreeData()!.buttons; - const sceneGraph = useDevtoolStore.use.sceneGraph()!; + const panelButtons = useDevtoolStore.use.sceneTreeData()?.buttons || []; + const sceneGraph = useDevtoolStore.use.sceneGraph(); const selectedNode = useDevtoolStore.use.selectedNode(); - const sceneGraphClone = JSON.parse(JSON.stringify(sceneGraph)); + const sceneGraphClone = JSON.parse(JSON.stringify(sceneGraph || {})); const [data, controller] = useSimpleTree(bridge, sceneGraphClone); const [currentSearch, setCurrentSearch] = useState(''); diff --git a/packages/frontend/src/pages/scene/scene-section/tree/context-menu-button.tsx b/packages/frontend/src/pages/scene/graph/tree/context-menu-button.tsx similarity index 89% rename from packages/frontend/src/pages/scene/scene-section/tree/context-menu-button.tsx rename to packages/frontend/src/pages/scene/graph/tree/context-menu-button.tsx index c81faba..41ed28b 100644 --- a/packages/frontend/src/pages/scene/scene-section/tree/context-menu-button.tsx +++ b/packages/frontend/src/pages/scene/graph/tree/context-menu-button.tsx @@ -27,7 +27,7 @@ export const CustomNodeContextMenuItem: React.FC<{ }> = ({ node, item, bridge, isLast }) => { const handleClick = useCallback(() => { bridge( - `window.__PIXI_DEVTOOLS_WRAPPER__?.tree.nodeContextMenu(${JSON.stringify(node.id)}, ${JSON.stringify(item.name)})`, + `window.__PIXI_DEVTOOLS_WRAPPER__?.scene.tree.nodeContextMenu(${JSON.stringify(node.id)}, ${JSON.stringify(item.name)})`, ); }, [node, item, bridge]); diff --git a/packages/frontend/src/pages/scene/scene-section/tree/cursor.tsx b/packages/frontend/src/pages/scene/graph/tree/cursor.tsx similarity index 100% rename from packages/frontend/src/pages/scene/scene-section/tree/cursor.tsx rename to packages/frontend/src/pages/scene/graph/tree/cursor.tsx diff --git a/packages/frontend/src/pages/scene/scene-section/tree/node-button.tsx b/packages/frontend/src/pages/scene/graph/tree/node-button.tsx similarity index 89% rename from packages/frontend/src/pages/scene/scene-section/tree/node-button.tsx rename to packages/frontend/src/pages/scene/graph/tree/node-button.tsx index 056adf0..46f8b47 100644 --- a/packages/frontend/src/pages/scene/scene-section/tree/node-button.tsx +++ b/packages/frontend/src/pages/scene/graph/tree/node-button.tsx @@ -61,8 +61,8 @@ export const CustomNodeButton: React.FC< (e: React.MouseEvent | boolean) => { const action = typeof e === 'boolean' - ? `window.__PIXI_DEVTOOLS_WRAPPER__?.tree.nodeButtonPress(${JSON.stringify(node.id)}, ${JSON.stringify(button.name)}, ${JSON.stringify(e)})` - : `window.__PIXI_DEVTOOLS_WRAPPER__?.tree.nodeButtonPress(${JSON.stringify(node.id)}, ${JSON.stringify(button.name)})`; + ? `window.__PIXI_DEVTOOLS_WRAPPER__?.scene.tree.nodeButtonPress(${JSON.stringify(node.id)}, ${JSON.stringify(button.name)}, ${JSON.stringify(e)})` + : `window.__PIXI_DEVTOOLS_WRAPPER__?.scene.tree.nodeButtonPress(${JSON.stringify(node.id)}, ${JSON.stringify(button.name)})`; bridge(action); if (e instanceof MouseEvent) { diff --git a/packages/frontend/src/pages/scene/scene-section/tree/node-trigger.tsx b/packages/frontend/src/pages/scene/graph/tree/node-trigger.tsx similarity index 94% rename from packages/frontend/src/pages/scene/scene-section/tree/node-trigger.tsx rename to packages/frontend/src/pages/scene/graph/tree/node-trigger.tsx index ca9ad8f..771f90a 100644 --- a/packages/frontend/src/pages/scene/scene-section/tree/node-trigger.tsx +++ b/packages/frontend/src/pages/scene/graph/tree/node-trigger.tsx @@ -49,10 +49,10 @@ export const NodeTrigger: React.FC<{ className={cn('mb-1 flex h-full min-w-max items-center gap-2 leading-5', node.state)} onDoubleClick={onToggle} onMouseEnter={() => { - bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.overlay.highlight(${JSON.stringify(node.id)})`); + bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.overlay.highlight(${JSON.stringify(node.id)})`); }} onMouseLeave={() => { - bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.overlay.highlight()`); + bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.overlay.highlight()`); }} > diff --git a/packages/frontend/src/pages/scene/scene-section/tree/node.tsx b/packages/frontend/src/pages/scene/graph/tree/node.tsx similarity index 100% rename from packages/frontend/src/pages/scene/scene-section/tree/node.tsx rename to packages/frontend/src/pages/scene/graph/tree/node.tsx diff --git a/packages/frontend/src/pages/scene/scene-section/tree/simple-tree.tsx b/packages/frontend/src/pages/scene/graph/tree/simple-tree.tsx similarity index 79% rename from packages/frontend/src/pages/scene/scene-section/tree/simple-tree.tsx rename to packages/frontend/src/pages/scene/graph/tree/simple-tree.tsx index bb4cfc9..d3a14c4 100644 --- a/packages/frontend/src/pages/scene/scene-section/tree/simple-tree.tsx +++ b/packages/frontend/src/pages/scene/graph/tree/simple-tree.tsx @@ -32,7 +32,7 @@ export function useSimpleTree(bridge: BridgeFn, initi tree.move({ id: args.dragIds[0], parentId: args.parentId, index: args.index }); bridge( - `window.__PIXI_DEVTOOLS_WRAPPER__?.tree.moveNode(${JSON.stringify(node.id)}, ${JSON.stringify(parent.id)}, ${JSON.stringify(args.index)})`, + `window.__PIXI_DEVTOOLS_WRAPPER__?.scene.tree.moveNode(${JSON.stringify(node.id)}, ${JSON.stringify(parent.id)}, ${JSON.stringify(args.index)})`, ); setData(tree.data); }; @@ -41,7 +41,9 @@ export function useSimpleTree(bridge: BridgeFn, initi tree.update({ id, changes: { name } as any }); setData(tree.data); const node = tree.find(id); - bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.tree.renameNode(${JSON.stringify(node?.id)}, ${JSON.stringify(name)})`); + bridge( + `window.__PIXI_DEVTOOLS_WRAPPER__?.scene.tree.renameNode(${JSON.stringify(node?.id)}, ${JSON.stringify(name)})`, + ); }; // TODO: allow for creating new nodes @@ -60,15 +62,15 @@ export function useSimpleTree(bridge: BridgeFn, initi args.ids.forEach((id) => tree.drop({ id })); setData(tree.data); - bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.tree.deleteNode(${JSON.stringify(node?.id)})`); + bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.tree.deleteNode(${JSON.stringify(node?.id)})`); if (!node || !node.parent) return; - bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.tree.setSelected(${JSON.stringify(node.parent.id)})`); + bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.tree.setSelected(${JSON.stringify(node.parent.id)})`); }; const onSelect = (nodes: NodeApi[]) => { const node = nodes[0]; if (!node) return; - bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.tree.setSelected(${node ? JSON.stringify(node.id) : null})`); + bridge(`window.__PIXI_DEVTOOLS_WRAPPER__?.scene.tree.setSelected(${node ? JSON.stringify(node.id) : null})`); }; const controller = { onMove, onRename, onDelete, onSelect }; diff --git a/packages/frontend/src/pages/scene/state.ts b/packages/frontend/src/pages/scene/scene.ts similarity index 74% rename from packages/frontend/src/pages/scene/state.ts rename to packages/frontend/src/pages/scene/scene.ts index 130113b..6449b67 100644 --- a/packages/frontend/src/pages/scene/state.ts +++ b/packages/frontend/src/pages/scene/scene.ts @@ -1,6 +1,27 @@ import type { ZustSet } from '../../lib/utils'; -import type { PropertyPanelData } from '../../components/properties/propertyTypes'; +import type { ALPHA_MODES, TEXTURE_DIMENSIONS, TEXTURE_FORMATS } from 'pixi.js'; import type { RemoveSetters } from '../../types'; +import type { PropertyPanelData } from '../../components/properties/propertyTypes'; + +export interface SceneDataState { + gpuSize: number; + pixelWidth: number; + pixelHeight: number; + width: number; + height: number; + mipLevelCount: number; + autoGenerateMipmaps: boolean; + format: TEXTURE_FORMATS; + dimension: TEXTURE_DIMENSIONS; + alphaMode: ALPHA_MODES; + antialias: boolean; + destroyed: boolean; + isPowerOfTwo: boolean; + autoGarbageCollect: boolean; + blob: string | null; + isLoaded: boolean; + name: string; +} export interface SceneState { stats: Record | null; diff --git a/packages/frontend/src/pages/scene/stats-section/Stats.tsx b/packages/frontend/src/pages/scene/stats-section/Stats.tsx deleted file mode 100644 index 095a9f3..0000000 --- a/packages/frontend/src/pages/scene/stats-section/Stats.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { memo, useEffect, useMemo, useRef } from 'react'; -import { useDevtoolStore } from '../../../App'; -import SmoothieComponent, { TimeSeries } from '../../../components/smooth-charts/Smoothie'; -import { useTheme } from '../../../components/theme-provider'; -import { SaveCollapsibleSection } from '../../../components/collapsible/collapsible-section'; - -const SmoothieStat: React.FC<{ - item: StatData; - stat: number; - seriesFillStyle: string; - defaultSmoothieOptions: any; -}> = memo(({ item, stat, seriesFillStyle, defaultSmoothieOptions }) => { - const seriesOptions = useMemo( - () => [ - { - data: item.timeSeries, - strokeStyle: item.color, - fillStyle: seriesFillStyle, - lineWidth: 1, - }, - ], - [item.timeSeries, item.color, seriesFillStyle], - ); - - return ( -
- - {item.name} ({stat}) - - -
- ); -}); - -interface StatData { - timeSeries: TimeSeries; - name: string; - color: string; -} - -const defaultSmoothieOptions = { - width: 250, - grid: { - strokeStyle: 'transparent', - fillStyle: 'transparent', - }, - labels: { - fillStyle: 'rgb(255, 255, 255)', - precision: 0, - }, - millisPerPixel: 60, - height: 30, -}; - -const updateGraph = (timeSeries: TimeSeries, numContainers: number) => { - timeSeries.append(new Date().getTime(), numContainers); -}; - -let currentStats = [] as StatData[]; -export const Stats: React.FC = () => { - const { theme } = useTheme(); - const stats = useDevtoolStore.use.stats(); - const animationRef = useRef(); - - defaultSmoothieOptions.labels.fillStyle = theme === 'dark' ? 'rgb(255, 255, 255)' : 'rgb(0, 0, 0)'; - const seriesFillStyle = theme === 'dark' ? 'rgba(231, 34, 100, 0.2)' : 'rgba(231, 34, 100, 0.8)'; - - // make sure every stat has a time series, and delete any that are no longer in the stats - currentStats = Object.keys(stats ?? {}).map((stat) => { - const existingStat = currentStats.find((item) => item.name === stat); - if (existingStat) { - return existingStat; - } else { - return { - timeSeries: new TimeSeries(), - name: stat, - color: '#E72264', - }; - } - }); - - useEffect(() => { - animationRef.current && cancelAnimationFrame(animationRef.current); - - let savedTime = 0; - const loop = () => { - const currentTime = Date.now(); - - if (currentTime - 250 > savedTime) { - savedTime = currentTime; - currentStats.forEach((item) => { - updateGraph(item.timeSeries, stats![item.name]); - }); - } - - animationRef.current = requestAnimationFrame(loop); - }; - - // Start the loop - animationRef.current = requestAnimationFrame(loop); - - // Clean up on unmount - return () => { - cancelAnimationFrame(animationRef.current!); - }; - }, [stats]); - - return ( - -
-
- {currentStats.map((item) => { - return ( - - ); - })} -
-
-
- ); -}; diff --git a/packages/frontend/src/types.ts b/packages/frontend/src/types.ts index b596fac..c64b90e 100644 --- a/packages/frontend/src/types.ts +++ b/packages/frontend/src/types.ts @@ -1,7 +1,7 @@ import type { BridgeFn } from './lib/utils'; import type { TextureState } from './pages/assets/assets'; import type { PermanentRenderingStateKeys, RenderingState } from './pages/rendering/rendering'; -import type { SceneState } from './pages/scene/state'; +import type { SceneState } from './pages/scene/scene'; import type { ButtonMetadata, PixiMetadata } from '@pixi/devtools'; export enum DevtoolMessage { @@ -14,6 +14,8 @@ export enum DevtoolMessage { panelHidden = 'devtool:panelHidden', pageReload = 'devtool:pageReload', + + overlayStateUpdate = 'pixi-overlay-state-update', } export type SceneGraphEntry = { @@ -23,10 +25,14 @@ export type SceneGraphEntry = { children: SceneGraphEntry[]; }; -export interface DevtoolState extends SceneState, TextureState, RenderingState { +export interface GlobalDevtoolState { active: boolean; setActive: (active: DevtoolState['active']) => void; + version: string | null; + setVersion: (version: DevtoolState['version']) => void; +} +export interface DevtoolState extends GlobalDevtoolState, SceneState, TextureState, RenderingState { chromeProxy: typeof chrome | null; setChromeProxy: (chromeProxy: DevtoolState['chromeProxy']) => void; diff --git a/tsconfig.json b/tsconfig.json index bb258d7..b32d0c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,8 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "baseUrl": "./", "paths": { "@devtool/frontend/*": ["packages/frontend/src/*"],