├── .editorconfig
├── .eslintrc.cjs
├── .gitattributes
├── .github
├── FUNDING.yml
├── logo.svg
├── pixi-active.svg
├── pixi-inactive.svg
└── workflows
│ ├── deploy-docs.yaml
│ └── main.yml
├── .gitignore
├── .husky
└── pre-commit
├── .prettierrc
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── packages
├── api
│ ├── README.md
│ ├── global.d.ts
│ ├── package.json
│ ├── src
│ │ ├── extensions
│ │ │ ├── ext.ts
│ │ │ ├── overlay.ts
│ │ │ ├── properties.ts
│ │ │ ├── stats.ts
│ │ │ └── tree.ts
│ │ ├── index.ts
│ │ └── types.ts
│ ├── tsconfig.json
│ └── vite.config.ts
├── backend
│ ├── package.json
│ ├── src
│ │ ├── assets
│ │ │ └── gpuTextures
│ │ │ │ └── textures.ts
│ │ ├── extensions
│ │ │ ├── Extensions.ts
│ │ │ └── getExtension.ts
│ │ ├── handler.ts
│ │ ├── pixi.ts
│ │ ├── rendering
│ │ │ ├── instructions.ts
│ │ │ ├── program.ts
│ │ │ ├── readPixels.ts
│ │ │ ├── renderableData.ts
│ │ │ ├── rendering.ts
│ │ │ └── stats.ts
│ │ ├── scene
│ │ │ ├── overlay
│ │ │ │ ├── overlay.ts
│ │ │ │ └── overlayExtension.ts
│ │ │ ├── scene.ts
│ │ │ ├── stats
│ │ │ │ ├── stats.ts
│ │ │ │ └── statsExtension.ts
│ │ │ └── tree
│ │ │ │ ├── extensions
│ │ │ │ ├── animatedSprite
│ │ │ │ │ ├── animatedSpritePropertyExtension.ts
│ │ │ │ │ └── sharedAnimatedSpriteProps.ts
│ │ │ │ ├── container
│ │ │ │ │ ├── containerPropertyExtension.ts
│ │ │ │ │ ├── sharedContainerProps.ts
│ │ │ │ │ ├── v7ContainerProps.ts
│ │ │ │ │ └── v8ContainerProps.ts
│ │ │ │ ├── ninesliceSprite
│ │ │ │ │ ├── ninesliceSpritePropertyExtension.ts
│ │ │ │ │ ├── sharedNinesliceSpriteProps.ts
│ │ │ │ │ ├── v7NinesliceSpriteProps.ts
│ │ │ │ │ └── v8NinesliceSpriteProps.ts
│ │ │ │ ├── text
│ │ │ │ │ ├── textPropertyExtension.ts
│ │ │ │ │ └── textProps.ts
│ │ │ │ ├── tilingSprite
│ │ │ │ │ ├── sharedTilingSpriteProps.ts
│ │ │ │ │ ├── tilingSpritePropertyExtension.ts
│ │ │ │ │ ├── v7TilingSpriteProps.ts
│ │ │ │ │ └── v8TilingSpriteProps.ts
│ │ │ │ ├── utils
│ │ │ │ │ ├── convertProp.ts
│ │ │ │ │ └── getProps.ts
│ │ │ │ └── view
│ │ │ │ │ ├── viewPropertyExtension.ts
│ │ │ │ │ └── viewProps.ts
│ │ │ │ ├── properties.ts
│ │ │ │ └── tree.ts
│ │ └── utils
│ │ │ ├── getPixiType.ts
│ │ │ ├── loop.ts
│ │ │ ├── poller.ts
│ │ │ └── throttle.ts
│ └── tsconfig.json
├── devtool-chrome
│ ├── assets
│ │ ├── contentStyle.css
│ │ ├── pixi-icon-active-128.png
│ │ ├── pixi-icon-active-16.png
│ │ ├── pixi-icon-active-48.png
│ │ ├── pixi-icon-inactive-128.png
│ │ ├── pixi-icon-inactive-16.png
│ │ └── pixi-icon-inactive-48.png
│ ├── manifest.dev.json
│ ├── manifest.json
│ ├── package.json
│ ├── postcss.config.js
│ ├── src
│ │ ├── background
│ │ │ └── index.ts
│ │ ├── content
│ │ │ └── index.ts
│ │ ├── devtools
│ │ │ ├── devtools.ts
│ │ │ ├── panel
│ │ │ │ ├── panel.html
│ │ │ │ └── panel.tsx
│ │ │ └── pixi-index.html
│ │ ├── inject
│ │ │ ├── close.ts
│ │ │ └── index.ts
│ │ └── messageUtils.ts
│ ├── tailwind.config.js
│ ├── vite.chrome.config.ts
│ └── vite.inject.config.ts
├── devtool-local
│ ├── index.html
│ ├── package.json
│ ├── postcss.config.js
│ ├── src
│ │ ├── main.tsx
│ │ └── scene.ts
│ ├── tailwind.config.js
│ └── vite.config.ts
├── docs
│ ├── .gitignore
│ ├── README.md
│ ├── babel.config.js
│ ├── docs
│ │ ├── guide
│ │ │ ├── faq.md
│ │ │ ├── features
│ │ │ │ ├── _category_.yml
│ │ │ │ ├── assets.mdx
│ │ │ │ └── scene.mdx
│ │ │ └── installation.mdx
│ │ └── plugin
│ │ │ ├── index.md
│ │ │ ├── overlay.md
│ │ │ ├── properties.md
│ │ │ ├── stats.md
│ │ │ └── tree.md
│ ├── docusaurus.config.ts
│ ├── package.json
│ ├── sidebars.ts
│ ├── src
│ │ ├── css
│ │ │ └── custom.css
│ │ └── pages
│ │ │ ├── index.module.css
│ │ │ └── index.tsx
│ ├── static
│ │ ├── .nojekyll
│ │ ├── gif
│ │ │ ├── devtool-asset-search.gif
│ │ │ ├── devtool-asset-selection.gif
│ │ │ ├── devtool-asset-sorting.gif
│ │ │ ├── devtool-copy.gif
│ │ │ ├── devtool-delete.gif
│ │ │ ├── devtool-highlight.gif
│ │ │ ├── devtool-lock.gif
│ │ │ ├── devtool-properties.gif
│ │ │ ├── devtool-rename.gif
│ │ │ ├── devtool-reparent.gif
│ │ │ ├── devtool-right-click.gif
│ │ │ ├── devtool-search.gif
│ │ │ ├── devtool-selection.gif
│ │ │ └── devtool-stats.gif
│ │ ├── img
│ │ │ ├── devtool-screenshot.png
│ │ │ ├── docusaurus.png
│ │ │ ├── favicon.png
│ │ │ ├── logo-chrome.svg
│ │ │ ├── logo-light.svg
│ │ │ ├── logo-main.svg
│ │ │ ├── logo.svg
│ │ │ ├── ogimage.png
│ │ │ ├── undraw_docusaurus_mountain.svg
│ │ │ ├── undraw_docusaurus_react.svg
│ │ │ └── undraw_docusaurus_tree.svg
│ │ └── social
│ │ │ ├── discord.svg
│ │ │ ├── github.svg
│ │ │ ├── open-col-icon.svg
│ │ │ └── twitter.svg
│ └── tsconfig.json
├── example
│ ├── index.html
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── vite.config.ts
└── frontend
│ ├── components.json
│ ├── package.json
│ ├── src
│ ├── App.tsx
│ ├── assets
│ │ ├── icon-active-48.png
│ │ ├── react.svg
│ │ ├── transparent-light.svg
│ │ └── transparent.svg
│ ├── components
│ │ ├── collapsible
│ │ │ ├── collapsible-section.tsx
│ │ │ └── collapsible-split.tsx
│ │ ├── mode-toggle.tsx
│ │ ├── navbar
│ │ │ └── navbar.tsx
│ │ ├── properties
│ │ │ ├── boolean-property.tsx
│ │ │ ├── button-property.tsx
│ │ │ ├── color-property.tsx
│ │ │ ├── multi-property.tsx
│ │ │ ├── number-property.tsx
│ │ │ ├── propertyEntry.tsx
│ │ │ ├── propertyTypes.tsx
│ │ │ ├── range-property.tsx
│ │ │ ├── select-property.tsx
│ │ │ ├── text-property.tsx
│ │ │ └── vector-property.tsx
│ │ ├── smooth-charts
│ │ │ └── stat.tsx
│ │ ├── theme-provider.tsx
│ │ └── ui
│ │ │ ├── button.tsx
│ │ │ ├── checkbox.tsx
│ │ │ ├── clipboard.tsx
│ │ │ ├── codearea.tsx
│ │ │ ├── color.tsx
│ │ │ ├── context-menu.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── input.tsx
│ │ │ ├── popover.tsx
│ │ │ ├── radio-group.tsx
│ │ │ ├── select.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── slider.tsx
│ │ │ ├── svg-texture.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── textarea.tsx
│ │ │ ├── toggle-group.tsx
│ │ │ ├── toggle.tsx
│ │ │ ├── tooltip.tsx
│ │ │ └── vector2.tsx
│ ├── globals.css
│ ├── lib
│ │ ├── interval.tsx
│ │ ├── localStorage.tsx
│ │ └── utils.ts
│ ├── pages
│ │ ├── assets
│ │ │ ├── AssetsPanel.tsx
│ │ │ ├── assets.ts
│ │ │ └── gpuTextures
│ │ │ │ ├── GPUTextures.tsx
│ │ │ │ ├── TextureProperties.tsx
│ │ │ │ ├── TextureStats.tsx
│ │ │ │ └── TextureViewer.tsx
│ │ ├── rendering
│ │ │ ├── CanvasPanel.tsx
│ │ │ ├── InstructionsPanel.tsx
│ │ │ ├── RenderingPanel.tsx
│ │ │ ├── RenderingStats.tsx
│ │ │ ├── instructions
│ │ │ │ ├── Batch.tsx
│ │ │ │ ├── CustomRender.tsx
│ │ │ │ ├── Filter.tsx
│ │ │ │ ├── Graphics.tsx
│ │ │ │ ├── Instructions.tsx
│ │ │ │ ├── Mask.tsx
│ │ │ │ ├── Mesh.tsx
│ │ │ │ ├── NineSliceSprite.tsx
│ │ │ │ ├── RenderGroup.tsx
│ │ │ │ ├── TilingSprite.tsx
│ │ │ │ ├── shared
│ │ │ │ │ ├── InstructionBubble.tsx
│ │ │ │ │ ├── InstructionSection.tsx
│ │ │ │ │ ├── PropertyDisplay.tsx
│ │ │ │ │ ├── Shader.tsx
│ │ │ │ │ ├── State.tsx
│ │ │ │ │ └── Texture.tsx
│ │ │ │ └── test.tsx
│ │ │ └── rendering.ts
│ │ └── scene
│ │ │ ├── ScenePanel.tsx
│ │ │ ├── SceneStats.tsx
│ │ │ ├── graph
│ │ │ ├── SceneProperties.tsx
│ │ │ ├── SceneTree.tsx
│ │ │ └── tree
│ │ │ │ ├── context-menu-button.tsx
│ │ │ │ ├── cursor.tsx
│ │ │ │ ├── node-button.tsx
│ │ │ │ ├── node-trigger.tsx
│ │ │ │ ├── node.tsx
│ │ │ │ └── simple-tree.tsx
│ │ │ └── scene.ts
│ ├── types.ts
│ └── vite-env.d.ts
│ ├── tailwind.config.js
│ └── tsconfig.json
├── scripts
├── release.mts
└── utils
│ ├── bump.mts
│ ├── json.mts
│ └── spawn.mts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Top-most EditorConfig file
2 | root = true
3 |
4 | # All files
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | 'prettier',
9 | 'plugin:prettier/recommended',
10 | ],
11 | ignorePatterns: ['dist', '.eslintrc.cjs'],
12 | parser: '@typescript-eslint/parser',
13 | plugins: ['react-refresh', 'import'],
14 | rules: {
15 | '@typescript-eslint/no-explicit-any': 'off',
16 | '@typescript-eslint/consistent-type-imports': ['error', { disallowTypeAnnotations: false }],
17 | 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
18 | 'import/no-duplicates': ['error'],
19 | '@typescript-eslint/no-unused-vars': [
20 | 'error',
21 | {
22 | args: 'all',
23 | argsIgnorePattern: '^_',
24 | caughtErrors: 'none',
25 | caughtErrorsIgnorePattern: '^_',
26 | destructuredArrayIgnorePattern: '^_',
27 | varsIgnorePattern: '^_',
28 | ignoreRestSiblings: true,
29 | },
30 | ],
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.js text eol=lf
2 | *.ts text eol=lf
3 | *.json text eol=lf
4 | *.yml text eol=lf
5 | *.md text eol=lf
6 | *.txt text eol=lf
7 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | open_collective: pixijs
2 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-docs.yaml:
--------------------------------------------------------------------------------
1 | name: Reusable Deploy
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - docs
8 |
9 | concurrency:
10 | group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | deploy:
15 | name: Deploy
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - uses: actions/setup-node@v4
21 | with:
22 | node-version: '20'
23 | cache: 'npm'
24 |
25 | - run: npm ci
26 | - run: |
27 | git config --global user.name "$GITHUB_ACTOR"
28 | git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com"
29 | git remote set-url origin https://git:${GIT_PASS}@github.com/pixijs/devtools.git
30 | npm run deploy
31 | env:
32 | GIT_USER: $GITHUB_ACTOR
33 | GIT_PASS: ${{ secrets.GITHUB_TOKEN }}
34 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Automation
2 | on:
3 | push:
4 | branches: ['**']
5 | release:
6 | types: [published]
7 | pull_request:
8 | branches: ['**']
9 | jobs:
10 | build:
11 | name: Build
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Install xvfb
16 | run: sudo apt-get install xvfb
17 | - name: Use Node.js 20.x
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: 20
21 | - name: Install Dependencies
22 | run: npm ci
23 |
24 | - name: Test Lint
25 | run: npm run lint
26 |
27 | - name: Test Types
28 | run: npm run types
29 |
30 | - name: Build for Distribution
31 | run: xvfb-run --auto-servernum npm run build
32 |
33 | - name: Upload chrome extension
34 | if: github.event_name != 'release'
35 | uses: actions/upload-artifact@v4
36 | with:
37 | name: upload
38 | path: .upload/
39 | include-hidden-files: true
40 |
41 | # Automatically attach files to release
42 | - name: Upload to Release
43 | if: github.event_name == 'release'
44 | uses: softprops/action-gh-release@v2
45 | with:
46 | files: .upload/*
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | .upload
27 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npx lint-staged
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "singleQuote": true,
4 | "trailingComma": "all",
5 | "tabWidth": 2,
6 | "semi": true,
7 | "plugins": ["prettier-plugin-tailwindcss"]
8 | }
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 PixiJS
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PixiJS DevTools
2 |
3 |
4 |
5 |
6 |
A chrome extension for debugging PixiJS applications.
7 |
8 |
9 |
10 |
11 | # Getting Started
12 |
13 | Please follow the documentation at [pixijs.io/devtools](https://pixijs.io/devtools)
14 |
15 | ## License
16 |
17 | MIT License.
18 |
--------------------------------------------------------------------------------
/packages/api/README.md:
--------------------------------------------------------------------------------
1 | # PixiJS DevTools
2 |
3 |
4 |
5 |
6 |
A chrome extension for debugging PixiJS applications.
7 |
8 |
9 |
10 |
11 | # Getting Started
12 |
13 | Please follow the documentation at [pixijs.io/devtools](https://pixijs.io/devtools)
14 |
15 | ## License
16 |
17 | MIT License.
18 |
--------------------------------------------------------------------------------
/packages/api/global.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | import type { Application, Container, Renderer } from 'pixi.js';
3 | import type { OverlayExtension } from './dist/extensions/overlay';
4 | import type { StatsExtension } from './dist/extensions/stats';
5 | import type { TreeExtension } from './dist/extensions/tree';
6 | import type { PropertiesExtension } from './dist/extensions/properties';
7 | import type { NodeExtension } from './dist/extensions/node';
8 | import type { PixiDevtools } from './dist/pixi';
9 |
10 | declare global {
11 | var __PIXI_APP_INIT__: (arg: Application | Renderer, version?: string) => void;
12 | var __PIXI_RENDERER_INIT__: (arg: Application | Renderer, version?: string) => void;
13 |
14 | interface Window {
15 | __PIXI_APP__: Application | undefined;
16 | __PIXI_STAGE__: Container | undefined;
17 | __PIXI_RENDERER__: Renderer | undefined;
18 | __PIXI__: typeof import('pixi.js');
19 | PIXI: typeof import('pixi.js');
20 | __PIXI_DEVTOOLS_WRAPPER__: PixiDevtools;
21 | __PIXI_DEVTOOLS__: {
22 | version?: string;
23 | pixi?: typeof import('pixi.js');
24 | app?: Application | undefined;
25 | stage?: Container | undefined;
26 | renderer?: Renderer | undefined;
27 | /** @deprecated since 2.0.0 */
28 | plugins?: any;
29 | extensions?: (OverlayExtension | StatsExtension | TreeExtension | PropertiesExtension | NodeExtension)[];
30 | };
31 | $pixi: Container | null;
32 | }
33 |
34 | namespace PixiMixins {
35 | interface Container {
36 | __devtoolIgnore?: boolean;
37 | __devtoolIgnoreChildren?: string;
38 | __devtoolLocked?: boolean;
39 | }
40 |
41 | interface WebGLSystems {
42 | __devtoolInjected?: boolean;
43 | }
44 |
45 | interface WebGPUSystems {
46 | __devtoolInjected?: boolean;
47 | }
48 | }
49 |
50 | namespace GlobalMixins {
51 | interface Container {
52 | __devtoolIgnore?: boolean;
53 | __devtoolIgnoreChildren?: string;
54 | __devtoolLocked?: boolean;
55 | }
56 | }
57 | }
58 |
59 | export {};
60 |
--------------------------------------------------------------------------------
/packages/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pixi/devtools",
3 | "publishConfig": {
4 | "access": "public"
5 | },
6 | "homepage": "https://pixijs.io/devtools/",
7 | "bugs": "https://github.com/pixijs/devtools/issues",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/pixijs/devtools.git"
11 | },
12 | "license": "MIT",
13 | "version": "2.2.1",
14 | "type": "module",
15 | "main": "dist/index.cjs",
16 | "module": "dist/index.js",
17 | "types": "dist/index.d.ts",
18 | "exports": {
19 | ".": {
20 | "types": "./dist/index.d.ts",
21 | "import": "./dist/index.js",
22 | "require": "./dist/index.cjs"
23 | }
24 | },
25 | "files": [
26 | "dist",
27 | "global.d.ts"
28 | ],
29 | "scripts": {
30 | "build": "vite build && tsc",
31 | "watch": "vite build --watch",
32 | "start": "npm run watch"
33 | },
34 | "devDependencies": {
35 | "pixi.js": "^8.2.2"
36 | },
37 | "peerDependencies": {
38 | "pixi.js": "^7 || ^8"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/packages/api/src/extensions/ext.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Collection of valid extension types.
3 | */
4 | export type ExtensionType = 'overlay' | 'stats' | 'sceneTree' | 'sceneProperties';
5 |
6 | export enum ExtensionPriority {
7 | Low = -1,
8 | Normal = 0,
9 | High = 1,
10 | }
11 |
12 | /**
13 | * The metadata for an extension.
14 | * @ignore
15 | */
16 | export interface ExtensionMetadataDetails {
17 | /** The extension type, can be multiple types */
18 | type: ExtensionType | ExtensionType[];
19 | /** Optional. Some extensions provide an API name/property, to make them more easily accessible */
20 | name?: string;
21 | /** Optional, used for sorting the extensions in a particular order */
22 | priority?: number;
23 | }
24 |
25 | /**
26 | * The metadata for an extension.
27 | */
28 | export type ExtensionMetadata = ExtensionType | ExtensionMetadataDetails;
29 |
--------------------------------------------------------------------------------
/packages/api/src/extensions/overlay.ts:
--------------------------------------------------------------------------------
1 | import type { Container } from 'pixi.js';
2 | import type { ExtensionMetadata } from './ext';
3 |
4 | export interface OverlayExtension {
5 | extension: ExtensionMetadata;
6 | /**
7 | * Get the css style for the selected highlight overlay.
8 | * @param node The selected node.
9 | */
10 | getSelectedStyle?(node: Container | null): Partial;
11 | /**
12 | * Get the css style for the hover highlight overlay.
13 | * @param node The hovered node.
14 | */
15 | getHoverStyle?(node: Container | null): Partial;
16 | /**
17 | * Get the global position of the node.
18 | * @param node The currently selected node to get the global position of.
19 | */
20 | getGlobalBounds?(node: Container): { x: number; y: number; width: number; height: number };
21 | }
22 |
--------------------------------------------------------------------------------
/packages/api/src/extensions/properties.ts:
--------------------------------------------------------------------------------
1 | import type { Container } from 'pixi.js';
2 | import type { ExtensionMetadata } from './ext';
3 |
4 | export type MultiProps = {
5 | inputs: Omit[];
6 | };
7 |
8 | type PropertyPanelData = {
9 | value: any;
10 | prop: string;
11 | entry: {
12 | section: string;
13 | tooltip?: string;
14 | label?: string;
15 | type: 'boolean' | 'number' | 'range' | 'select' | 'text' | 'button' | 'vector2' | 'vectorX' | 'color' | 'multi';
16 | options?: any;
17 | };
18 | };
19 |
20 | export type PropertiesEntry = PropertyPanelData & { allowUndefined?: boolean; allowCopy?: boolean };
21 | export type Properties = Omit & { allowUndefined?: boolean; allowCopy?: boolean };
22 |
23 | export interface PropertiesExtension {
24 | extension: ExtensionMetadata;
25 | /**
26 | * Test if the plugin can handle the container.
27 | * @param container The current node.
28 | */
29 | testNode(container: Container): boolean;
30 | /**
31 | * Test if the plugin can handle the property.
32 | * @param prop The property name.
33 | */
34 | testProp(prop: string): boolean;
35 | /**
36 | * Set the property for the container.
37 | * @param container The current node.
38 | * @param prop The property name.
39 | * @param value The new value. This value will be an array if the property has multiple inputs. e.g position.
40 | */
41 | setProperty(container: Container, prop: string, value: any): void;
42 | /**
43 | * Get the properties for the container.
44 | * @param container The current node.
45 | */
46 | getProperties(container: Container): PropertiesEntry[];
47 | }
48 |
--------------------------------------------------------------------------------
/packages/api/src/extensions/stats.ts:
--------------------------------------------------------------------------------
1 | import type { Container } from 'pixi.js';
2 | import type { ExtensionMetadata } from './ext';
3 |
4 | export interface StatsExtension {
5 | extension: ExtensionMetadata;
6 | /**
7 | * Track some stats for a node.
8 | * This function is called on every node in the scene.
9 | * @param container The current node
10 | * @param state The current stats for the scene.
11 | */
12 | track: (container: Container, state: Record) => void;
13 | /**
14 | * Get the keys of what ypu are tracking.
15 | * This is used to warn the user if the same key is being tracked by multiple extensions.
16 | */
17 | getKeys: () => string[];
18 | }
19 |
--------------------------------------------------------------------------------
/packages/api/src/index.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/triple-slash-reference
2 | ///
3 | import type { Devtools } from './types';
4 |
5 | export * from './extensions/ext';
6 | export * from './extensions/overlay';
7 | export * from './extensions/properties';
8 | export * from './extensions/stats';
9 | export * from './extensions/tree';
10 | export * from './types';
11 |
12 | export async function initDevtools(opts: Devtools) {
13 | const options: Devtools = {
14 | importPixi: false,
15 | ...opts,
16 | };
17 |
18 | if (options.app) {
19 | options.renderer = options.app.renderer;
20 | options.stage = options.app.stage;
21 | }
22 |
23 | if (options.importPixi && !options.pixi) {
24 | options.pixi = await import('pixi.js');
25 | }
26 |
27 | window.__PIXI_DEVTOOLS__ = {
28 | ...(window.__PIXI_DEVTOOLS__ || {}),
29 | app: options.app,
30 | stage: options.stage,
31 | renderer: options.renderer,
32 | version: options.version,
33 | extensions: [...(window.__PIXI_DEVTOOLS__?.extensions || []), ...(options.extensions || [])],
34 | plugins: {},
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/packages/api/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { Container, Renderer, Application } from 'pixi.js';
2 | import type { OverlayExtension } from './extensions/overlay';
3 |
4 | /** @deprecated since 2.0.0 */
5 | export interface DevtoolApp extends DevtoolPixi {
6 | app: Application;
7 | }
8 |
9 | /** @deprecated since 2.0.0 */
10 | export interface DevtoolRenderer extends DevtoolPixi {
11 | renderer: Renderer;
12 | stage: Container;
13 | }
14 |
15 | interface DevtoolPixi {
16 | pixi?: typeof import('pixi.js');
17 | importPixi?: boolean;
18 | extensions?: OverlayExtension[];
19 | /** @deprecated since 2.0.0 */
20 | plugins?: any;
21 | }
22 |
23 | /** @deprecated since 2.0.0 */
24 | export type DevtoolsAPI = DevtoolApp | DevtoolRenderer;
25 |
26 | /**
27 | * The options for the devtools
28 | */
29 | export interface Devtools extends DevtoolPixi {
30 | app?: Application;
31 | renderer?: Renderer;
32 | stage?: Container;
33 | version?: string;
34 | }
35 |
--------------------------------------------------------------------------------
/packages/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "declaration": true,
5 | "emitDeclarationOnly": true,
6 | "noEmit": false,
7 | "outDir": "dist"
8 | },
9 | "include": ["src"]
10 | }
11 |
--------------------------------------------------------------------------------
/packages/api/vite.config.ts:
--------------------------------------------------------------------------------
1 | // vite.config.js
2 | import { resolve } from 'node:path';
3 | import { defineConfig } from 'vite';
4 |
5 | export default defineConfig({
6 | build: {
7 | lib: {
8 | // Could also be a dictionary or array of multiple entry points
9 | entry: resolve(__dirname, 'src/index.ts'),
10 | formats: ['es', 'cjs'],
11 | fileName: 'index',
12 | },
13 | rollupOptions: {
14 | // make sure to externalize deps that shouldn't be bundled
15 | // into your library
16 | external: ['pixi.js'],
17 | },
18 | },
19 | });
20 |
--------------------------------------------------------------------------------
/packages/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@devtool/backend",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "scripts": {},
7 | "dependencies": {}
8 | }
9 |
--------------------------------------------------------------------------------
/packages/backend/src/handler.ts:
--------------------------------------------------------------------------------
1 | import type { Container } from 'pixi.js';
2 | import type { PixiDevtools } from './pixi';
3 |
4 | export class PixiHandler {
5 | protected _devtool: typeof PixiDevtools;
6 | constructor(devtool: typeof PixiDevtools) {
7 | this._devtool = devtool;
8 | }
9 |
10 | public init() {}
11 | public reset() {}
12 | public preupdate() {}
13 | public update() {}
14 | public throttledUpdate() {}
15 | public loop(_container: Container) {}
16 | public postupdate() {}
17 | }
18 |
--------------------------------------------------------------------------------
/packages/backend/src/rendering/program.ts:
--------------------------------------------------------------------------------
1 | import type { Filter, TextureShader } from 'pixi.js';
2 |
3 | export function getProgramSource(
4 | filter: Filter | TextureShader,
5 | shader: 'fragment' | 'vertex',
6 | rendererType: 'webgl' | 'webgpu',
7 | ): string {
8 | const type = rendererType === 'webgl' ? 'glProgram' : 'gpuProgram';
9 | const program = filter[type];
10 | const source = program[shader];
11 |
12 | if (!source) {
13 | return '';
14 | }
15 |
16 | if (typeof source === 'string') {
17 | return source;
18 | }
19 |
20 | return source.source;
21 | }
22 |
--------------------------------------------------------------------------------
/packages/backend/src/rendering/readPixels.ts:
--------------------------------------------------------------------------------
1 | import type { GlRenderTarget, WebGLRenderer, WebGPURenderer } from 'pixi.js';
2 |
3 | export function readGlPixels(
4 | gl: WebGLRenderingContext,
5 | renderer: WebGLRenderer,
6 | canvasTextures: string[],
7 | width: number,
8 | height: number,
9 | ) {
10 | // Create a buffer to hold the pixel data
11 | // Uint8Array for 8-bit per channel (RGBA), adjust as needed
12 | const pixels = new Uint8Array(width * height * 4);
13 |
14 | const renterTarget = renderer.renderTarget.getRenderTarget(renderer.renderTarget.renderTarget);
15 | const glRenterTarget = renderer.renderTarget.getGpuRenderTarget(renterTarget) as GlRenderTarget;
16 | // Bind the framebuffer you want to read from (null for default framebuffer)
17 | gl.bindFramebuffer(gl.FRAMEBUFFER, glRenterTarget.resolveTargetFramebuffer);
18 |
19 | // Read the pixels
20 | gl.readPixels(
21 | 0,
22 | 0, // Start reading from the bottom left of the framebuffer
23 | width,
24 | height, // The dimensions of the area you want to read
25 | gl.RGBA, // Format of the pixel data
26 | gl.UNSIGNED_BYTE, // Type of the pixel data
27 | pixels, // The buffer to read the pixels into
28 | );
29 |
30 | // Create a 2D canvas to draw the pixels on
31 | const canvas2d = document.createElement('canvas');
32 | canvas2d.width = width;
33 | canvas2d.height = height;
34 | const ctx = canvas2d.getContext('2d')!;
35 |
36 | // Create an ImageData object
37 | const imageData = new ImageData(new Uint8ClampedArray(pixels), width, height);
38 |
39 | // Draw the ImageData object to the canvas
40 | ctx.putImageData(imageData, 0, 0);
41 |
42 | // Convert the canvas to a data URL and set it as the src of an image element
43 | const dataUrl = canvas2d.toDataURL('image/webp', 0.5);
44 | canvasTextures.push(dataUrl);
45 | }
46 |
47 | export function readGPUPixels(renderer: WebGPURenderer, canvasTextures: string[]) {
48 | const webGPUCanvas = renderer.view.canvas as HTMLCanvasElement;
49 |
50 | const canvas = document.createElement('canvas');
51 | canvas.width = webGPUCanvas.width;
52 | canvas.height = webGPUCanvas.height;
53 |
54 | const context = canvas.getContext('2d')!;
55 |
56 | context.drawImage(webGPUCanvas, 0, 0);
57 |
58 | // const { width, height } = webGPUCanvas;
59 |
60 | // context.getImageData(0, 0, width, height);
61 |
62 | // Convert the canvas to a data URL and set it as the src of an image element
63 | const dataUrl = canvas.toDataURL('image/webp', 0.5);
64 | canvasTextures.push(dataUrl);
65 |
66 | // const pixels = new Uint8ClampedArray(imageData.data.buffer);
67 |
68 | // return { pixels, width, height };
69 | }
70 |
--------------------------------------------------------------------------------
/packages/backend/src/rendering/stats.ts:
--------------------------------------------------------------------------------
1 | export class Stats {
2 | private frames: number = 0;
3 | private prevTime: number = 0;
4 | public fps: number = 0;
5 | public memory: number = 0;
6 | public maxMemory: number = 0;
7 | public drawCalls: number = 0;
8 |
9 | public update(): void {
10 | this.frames++;
11 |
12 | const time = (performance || Date).now();
13 |
14 | if (time >= this.prevTime + 1000) {
15 | this.fps = (this.frames * 1000) / (time - this.prevTime);
16 | this.prevTime = time;
17 | this.frames = 0;
18 |
19 | // @ts-expect-error it does exist in chrome
20 | const memory = performance.memory;
21 | this.memory = memory.usedJSHeapSize / 1048576;
22 | this.maxMemory = memory.jsHeapSizeLimit / 1048576;
23 | }
24 | }
25 |
26 | public reset(): void {
27 | this.frames = 0;
28 | this.prevTime = 0;
29 | this.fps = 0;
30 | this.memory = 0;
31 | this.maxMemory = 0;
32 | this.drawCalls = 0;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/overlay/overlayExtension.ts:
--------------------------------------------------------------------------------
1 | import type { OverlayExtension } from '@pixi/devtools';
2 | import type { Container } from 'pixi.js';
3 | import { isParticleContainer } from '../../utils/getPixiType';
4 |
5 | export const overlayExtension: OverlayExtension = {
6 | extension: {
7 | type: 'overlay',
8 | priority: -1,
9 | name: 'default-overlay',
10 | },
11 |
12 | getSelectedStyle() {
13 | return {
14 | backgroundColor: 'hsla(340 70% 44% / 35%)',
15 | border: '1px solid hsla(0, 0%, 100%, 0.5)',
16 | };
17 | },
18 |
19 | getHoverStyle() {
20 | return {
21 | backgroundColor: 'hsla(192 84% 40% / 40%)',
22 | border: '1px solid hsla(0, 0%, 100%, 0.5)',
23 | };
24 | },
25 |
26 | getGlobalBounds(node: Container) {
27 | if (isParticleContainer(node)) {
28 | if (node.boundsArea) return node.boundsArea;
29 | // unknown bounds
30 | return { x: 0, y: 0, width: 0, height: 0 };
31 | }
32 | return node.getBounds();
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/scene.ts:
--------------------------------------------------------------------------------
1 | import type { Container } from 'pixi.js';
2 | import { PixiHandler } from '../handler';
3 | import { Stats } from './stats/stats';
4 | import type { PixiDevtools } from '../pixi';
5 | import { Overlay } from './overlay/overlay';
6 | import { Tree } from './tree/tree';
7 | import { Properties } from './tree/properties';
8 |
9 | export class Scene extends PixiHandler {
10 | public stats: Stats;
11 | public overlay: Overlay;
12 | public tree: Tree;
13 | public properties: Properties;
14 |
15 | // scene state
16 | // public get stats(){
17 | // return this.stats.stats;
18 | // }
19 |
20 | constructor(devtool: typeof PixiDevtools) {
21 | super(devtool);
22 | this.stats = new Stats(devtool);
23 | this.overlay = new Overlay(devtool);
24 | this.tree = new Tree(devtool);
25 | this.properties = new Properties(devtool);
26 | }
27 |
28 | public override init() {
29 | this.stats.init();
30 | this.overlay.init();
31 | this.tree.init();
32 | this.properties.init();
33 | }
34 | public override reset() {
35 | this.stats.reset();
36 | this.overlay.reset();
37 | this.tree.reset();
38 | this.properties.reset();
39 | }
40 | public override preupdate() {
41 | this.stats.preupdate();
42 | this.overlay.preupdate();
43 | this.tree.preupdate();
44 | this.properties.preupdate();
45 | }
46 | public override loop(container: Container) {
47 | this.stats.loop(container);
48 | this.tree.loop(container);
49 | }
50 | public override postupdate() {
51 | this.stats.postupdate();
52 | this.overlay.postupdate();
53 | this.tree.postupdate();
54 | this.properties.postupdate();
55 | }
56 |
57 | public override throttledUpdate() {
58 | this.stats.throttledUpdate();
59 | this.overlay.throttledUpdate();
60 | this.tree.throttledUpdate();
61 | this.properties.throttledUpdate();
62 | }
63 | public override update() {
64 | this.stats.update();
65 | this.overlay.update();
66 | this.tree.update();
67 | this.properties.update();
68 | }
69 |
70 | public getStats() {
71 | return this.stats.stats;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/stats/stats.ts:
--------------------------------------------------------------------------------
1 | import type { StatsExtension } from '@pixi/devtools';
2 | import type { Container } from 'pixi.js';
3 | import { extensions } from '../../extensions/Extensions';
4 | import { getExtensionsProp } from '../../extensions/getExtension';
5 | import { PixiHandler } from '../../handler';
6 |
7 | export class Stats extends PixiHandler {
8 | public static extensions: StatsExtension[] = [];
9 | private _extensions: Required[] = [];
10 |
11 | public stats: Record = {};
12 |
13 | public override init() {
14 | this._extensions = getExtensionsProp(Stats.extensions, 'track');
15 | const allKeys: string[] = [];
16 | for (const plugin of this._extensions) {
17 | allKeys.push(...plugin.getKeys());
18 | }
19 |
20 | // check for duplicates
21 | const duplicates = allKeys.filter((item, index) => allKeys.indexOf(item) !== index);
22 |
23 | if (duplicates.length > 0) {
24 | console.warn(`[PixiJS Devtools] Stats: Duplicate keys found: ${duplicates.join(', ')}`);
25 | }
26 | }
27 |
28 | public override reset() {
29 | this.stats = {};
30 | }
31 |
32 | public override throttledUpdate(): void {
33 | this.stats = {};
34 | }
35 |
36 | public override loop(container: Container) {
37 | for (const plugin of this._extensions) {
38 | plugin.track(container, this.stats);
39 | }
40 | }
41 |
42 | public override postupdate() {
43 | for (const key in this.stats) {
44 | // also format the keys to be more readable
45 | const formattedKey = this.formatCamelCase(key);
46 | this.stats[formattedKey] = this.stats[key];
47 | delete this.stats[key];
48 | }
49 | }
50 |
51 | private formatCamelCase(string: string) {
52 | let result = string.replace(/([A-Z])/g, ' $1');
53 | result = result.charAt(0).toUpperCase() + result.slice(1);
54 | return result;
55 | }
56 | }
57 |
58 | extensions.handleByList('stats', Stats.extensions);
59 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/stats/statsExtension.ts:
--------------------------------------------------------------------------------
1 | import type { DevtoolState } from '@devtool/frontend/types';
2 | import type { StatsExtension } from '@pixi/devtools';
3 | import type { Container } from 'pixi.js';
4 | import { getPixiType } from '../../utils/getPixiType';
5 |
6 | export const totalStatsExtension: StatsExtension = {
7 | extension: {
8 | type: 'stats',
9 | name: 'default-stats-total',
10 | },
11 |
12 | track: (_container: Container, state: NonNullable) => {
13 | state.total = (state.total || 0) + 1;
14 | },
15 |
16 | getKeys: () => {
17 | return ['total'];
18 | },
19 | };
20 |
21 | const typeMap = {
22 | Container: 'container',
23 | Sprite: 'sprite',
24 | Graphics: 'graphics',
25 | Mesh: 'mesh',
26 | Text: 'text',
27 | BitmapText: 'bitmapText',
28 | HTMLText: 'htmlText',
29 | AnimatedSprite: 'animatedSprite',
30 | NineSliceSprite: 'nineSliceSprite',
31 | TilingSprite: 'tilingSprite',
32 | ParticleContainer: 'particleContainer',
33 | };
34 |
35 | export const pixiStatsExtension = {
36 | extension: {
37 | type: 'stats',
38 | name: 'default-stats-pixi',
39 | },
40 | track(container: Container, state: NonNullable) {
41 | const type = getPixiType(container) as keyof typeof typeMap;
42 | const mappedType = typeMap[type];
43 |
44 | if (mappedType) {
45 | state[mappedType] = (state[mappedType] || 0) + 1;
46 | }
47 | },
48 | getKeys: () => {
49 | return Object.values(typeMap);
50 | },
51 | } satisfies StatsExtension;
52 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/animatedSprite/animatedSpritePropertyExtension.ts:
--------------------------------------------------------------------------------
1 | import type { PropertiesEntry } from '@pixi/devtools';
2 | import type { AnimatedSprite } from 'pixi.js';
3 | import { PixiDevtools } from '../../../../pixi';
4 | import { isAnimatedSprite } from '../../../../utils/getPixiType';
5 | import type { DefaultPropertyExtension } from '../container/containerPropertyExtension';
6 | import { sharedAnimatedSpriteProps } from './sharedAnimatedSpriteProps';
7 |
8 | export const animatedSpritePropertyExtension: DefaultPropertyExtension = {
9 | extension: {
10 | type: 'sceneProperties',
11 | name: 'default-animated-sprite-properties',
12 | priority: 0,
13 | },
14 | properties() {
15 | return sharedAnimatedSpriteProps;
16 | },
17 | testNode(container: AnimatedSprite) {
18 | return isAnimatedSprite(container, PixiDevtools.pixi);
19 | },
20 | testProp(prop: string) {
21 | // test if the property is in the list
22 | return this.properties().some((p) => prop.startsWith(p.prop));
23 | },
24 | getProperties(container: AnimatedSprite) {
25 | // get the properties from the container
26 | const activeProps = this.properties().reduce((result, property) => {
27 | const prop = property.prop;
28 | let value = container[prop as keyof AnimatedSprite];
29 |
30 | if (value != null && (prop === 'gotoAndPlay' || prop === 'stop' || prop === 'play')) {
31 | value = true;
32 | }
33 |
34 | if (value == null && property.allowUndefined !== true) {
35 | return result;
36 | }
37 |
38 | result.push({
39 | ...property,
40 | value,
41 | });
42 |
43 | return result;
44 | }, [] as PropertiesEntry[]);
45 |
46 | return activeProps;
47 | },
48 | setProperty(container: AnimatedSprite, prop, value) {
49 | if (prop === 'gotoAndPlay') {
50 | container.gotoAndPlay(0);
51 | } else if (prop === 'stop') {
52 | container.stop();
53 | } else if (prop === 'play') {
54 | container.play();
55 | } else {
56 | (container as any)[prop] = value;
57 | }
58 | },
59 | };
60 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/animatedSprite/sharedAnimatedSpriteProps.ts:
--------------------------------------------------------------------------------
1 | import type { Properties } from '@pixi/devtools';
2 |
3 | export const sharedAnimatedSpriteProps = [
4 | { prop: 'animationSpeed', entry: { section: 'Animated Sprite', type: 'number' } },
5 | { prop: 'loop', entry: { section: 'Animated Sprite', type: 'boolean' } },
6 | { prop: 'updateAnchor', entry: { section: 'Animated Sprite', type: 'boolean' } },
7 | {
8 | prop: 'totalFrames',
9 | entry: { section: 'Animated Sprite', type: 'number', options: { disabled: true } },
10 | },
11 | { prop: 'currentFrame', entry: { section: 'Animated Sprite', type: 'number' } },
12 | { prop: 'autoUpdate', entry: { section: 'Animated Sprite', type: 'boolean' } },
13 | { prop: 'playing', entry: { section: 'Animated Sprite', type: 'boolean' } },
14 | {
15 | prop: 'gotoAndPlay',
16 | entry: { section: 'Animated Sprite', type: 'button', label: 'start' },
17 | },
18 | { prop: 'stop', entry: { section: 'Animated Sprite', type: 'button', label: 'stop' } },
19 | { prop: 'play', entry: { section: 'Animated Sprite', type: 'button', label: 'play' } },
20 | ] as Properties[];
21 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/container/v7ContainerProps.ts:
--------------------------------------------------------------------------------
1 | import type { Properties } from '@pixi/devtools';
2 | import { sharedContainerProps } from './sharedContainerProps';
3 |
4 | export const v7BlendModeMap = {
5 | normal: 0,
6 | add: 1,
7 | multiply: 2,
8 | screen: 3,
9 | overlay: 4,
10 | darken: 5,
11 | lighten: 6,
12 | colorDodge: 7,
13 | colorBurn: 8,
14 | hardLight: 9,
15 | softLight: 10,
16 | difference: 11,
17 | exclusion: 12,
18 | hue: 13,
19 | saturation: 14,
20 | color: 15,
21 | luminosity: 16,
22 | none: 20,
23 | };
24 |
25 | export const v7ContainerProps = [
26 | ...sharedContainerProps,
27 | { prop: 'name', entry: { section: 'Info', type: 'text' } },
28 | {
29 | prop: 'blendMode',
30 | entry: {
31 | section: 'Appearance',
32 | options: {
33 | options: [
34 | 'normal',
35 | 'add',
36 | 'multiply',
37 | 'screen',
38 | 'overlay',
39 | 'darken',
40 | 'lighten',
41 | 'color-dodge',
42 | 'color-burn',
43 | 'hard-light',
44 | 'soft-light',
45 | 'difference',
46 | 'exclusion',
47 | 'hue',
48 | 'saturation',
49 | 'color',
50 | 'luminosity',
51 | 'none',
52 | ],
53 | },
54 | type: 'select',
55 | },
56 | },
57 | ] as Properties[];
58 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/container/v8ContainerProps.ts:
--------------------------------------------------------------------------------
1 | import type { Properties } from '@pixi/devtools';
2 | import { sharedContainerProps } from './sharedContainerProps';
3 |
4 | export const v8ContainerProps = [
5 | ...sharedContainerProps,
6 | { prop: 'label', entry: { section: 'Info', type: 'text' } },
7 | {
8 | prop: 'blendMode',
9 | entry: {
10 | section: 'Appearance',
11 | tooltip: `To use certain modes, enable them as follows:\n{{import 'pixi.js/advanced-blend-modes';}}`,
12 | options: {
13 | options: [
14 | 'inherit',
15 | 'normal',
16 | 'add',
17 | 'multiply',
18 | 'screen',
19 | 'overlay',
20 | 'darken',
21 | 'lighten',
22 | 'color-dodge',
23 | 'color-burn',
24 | 'hard-light',
25 | 'soft-light',
26 | 'difference',
27 | 'exclusion',
28 | 'hue',
29 | 'saturation',
30 | 'color',
31 | 'luminosity',
32 | 'none',
33 | 'erase',
34 | 'subtract',
35 | 'linear-burn',
36 | 'linear-dodge',
37 | 'linear-light',
38 | 'pin-light',
39 | 'divide',
40 | 'vivid-light',
41 | 'hard-mix',
42 | 'negation',
43 | ],
44 | },
45 | type: 'select',
46 | },
47 | },
48 | {
49 | prop: 'boundsArea',
50 | entry: {
51 | section: 'Rendering',
52 | options: {
53 | inputs: [{ label: 'x' }, { label: 'y' }, { label: 'width' }, { label: 'height' }],
54 | },
55 | type: 'vectorX',
56 | },
57 | },
58 | { prop: 'isRenderGroup', entry: { section: 'Rendering', type: 'boolean' } },
59 |
60 | { prop: 'cullable', entry: { section: 'Culling', type: 'boolean' } },
61 | {
62 | prop: 'cullArea',
63 | entry: {
64 | section: 'Culling',
65 | options: {
66 | inputs: [{ label: 'x' }, { label: 'y' }, { label: 'width' }, { label: 'height' }],
67 | },
68 | type: 'vectorX',
69 | },
70 | },
71 | { prop: 'cullableChildren', entry: { section: 'Culling', type: 'boolean' } },
72 | ] as Properties[];
73 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/ninesliceSprite/ninesliceSpritePropertyExtension.ts:
--------------------------------------------------------------------------------
1 | import type { AnimatedSprite } from 'pixi.js';
2 | import { PixiDevtools } from '../../../../pixi';
3 | import { isNineSliceSprite } from '../../../../utils/getPixiType';
4 | import type { DefaultPropertyExtension } from '../container/containerPropertyExtension';
5 | import { getProps } from '../utils/getProps';
6 | import { v7NineSliceProps } from './v7NinesliceSpriteProps';
7 | import { v8NinesSliceProps } from './v8NinesliceSpriteProps';
8 |
9 | export const nineSlicePropertyExtension: DefaultPropertyExtension = {
10 | extension: {
11 | type: 'sceneProperties',
12 | name: 'default-nineslice-properties',
13 | priority: 0,
14 | },
15 | properties() {
16 | return getProps(v7NineSliceProps, v8NinesSliceProps);
17 | },
18 | testNode(container: AnimatedSprite) {
19 | return isNineSliceSprite(container, PixiDevtools.pixi);
20 | },
21 | testProp(prop: string) {
22 | // test if the property is in the list
23 | return this.properties().some((p) => prop.startsWith(p.prop));
24 | },
25 | getProperties(container: AnimatedSprite) {
26 | // get the properties from the container
27 | const activeProps = this.properties().reduce((result, property) => {
28 | const prop = property.prop;
29 | const value = container[prop as keyof AnimatedSprite];
30 |
31 | if (value == null && property.allowUndefined !== true) {
32 | return result;
33 | }
34 |
35 | result.push({
36 | ...property,
37 | value,
38 | });
39 |
40 | return result;
41 | }, [] as any[]);
42 |
43 | return activeProps;
44 | },
45 | setProperty(container: AnimatedSprite, prop, value) {
46 | (container as any)[prop] = value;
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/ninesliceSprite/sharedNinesliceSpriteProps.ts:
--------------------------------------------------------------------------------
1 | import type { Properties } from '@pixi/devtools';
2 |
3 | export const sharedNineSliceProps = [
4 | { prop: 'leftWidth', entry: { section: 'NineSlice Sprite', type: 'number' } },
5 | { prop: 'rightWidth', entry: { section: 'NineSlice Sprite', type: 'number' } },
6 | { prop: 'topHeight', entry: { section: 'NineSlice Sprite', type: 'number' } },
7 | {
8 | prop: 'bottomHeight',
9 | entry: {
10 | section: 'NineSlice Sprite',
11 | type: 'number',
12 | },
13 | },
14 | ] as Properties[];
15 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/ninesliceSprite/v7NinesliceSpriteProps.ts:
--------------------------------------------------------------------------------
1 | import type { Properties } from '@pixi/devtools';
2 | import { sharedViewProps } from '../view/viewProps';
3 |
4 | export const v7NineSliceProps = [...sharedViewProps] as Properties[];
5 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/ninesliceSprite/v8NinesliceSpriteProps.ts:
--------------------------------------------------------------------------------
1 | import type { Properties } from '@pixi/devtools';
2 | import { sharedNineSliceProps } from './sharedNinesliceSpriteProps';
3 |
4 | export const v8NinesSliceProps = [
5 | ...sharedNineSliceProps,
6 | {
7 | prop: 'originalWidth',
8 | entry: {
9 | section: 'NineSlice Sprite',
10 | type: 'number',
11 | },
12 | },
13 | {
14 | prop: 'originalHeight',
15 | entry: {
16 | section: 'NineSlice Sprite',
17 | type: 'number',
18 | },
19 | },
20 | ] as Properties[];
21 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/text/textPropertyExtension.ts:
--------------------------------------------------------------------------------
1 | import type { PropertiesEntry } from '@pixi/devtools';
2 | import type { Text } from 'pixi.js';
3 | import { PixiDevtools } from '../../../../pixi';
4 | import { isText } from '../../../../utils/getPixiType';
5 | import type { DefaultPropertyExtension } from '../container/containerPropertyExtension';
6 | import { textProps } from './textProps';
7 |
8 | export const textPropertyExtension: DefaultPropertyExtension = {
9 | extension: {
10 | type: 'sceneProperties',
11 | name: 'default-text-properties',
12 | priority: 0,
13 | },
14 | properties() {
15 | return textProps;
16 | },
17 | testNode(container: Text) {
18 | return isText(container, PixiDevtools.pixi);
19 | },
20 | testProp(prop: string) {
21 | // test if the property is in the list
22 | return this.properties().some((p) => prop.startsWith(p.prop));
23 | },
24 | getProperties(container: Text) {
25 | // get the properties from the container
26 | const activeProps = this.properties().reduce((result, property) => {
27 | const prop = property.prop;
28 | const value = container[prop as keyof Text];
29 |
30 | if (value == null && property.allowUndefined !== true) {
31 | return result;
32 | }
33 |
34 | result.push({
35 | ...property,
36 | value,
37 | });
38 |
39 | return result;
40 | }, [] as PropertiesEntry[]);
41 |
42 | return activeProps;
43 | },
44 | setProperty(container: Text, prop, value) {
45 | (container as any)[prop] = value;
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/text/textProps.ts:
--------------------------------------------------------------------------------
1 | import type { Properties } from '@pixi/devtools';
2 |
3 | export const textProps = [
4 | { prop: 'text', entry: { section: 'Text', type: 'text' } },
5 | // { section: 'Text', property: 'style', propertyProps: { label: 'Style' }, type: 'text' },
6 | { prop: 'resolution', entry: { section: 'Text', type: 'number' } },
7 | ] as Properties[];
8 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/tilingSprite/sharedTilingSpriteProps.ts:
--------------------------------------------------------------------------------
1 | import type { Properties } from '@pixi/devtools';
2 |
3 | export const sharedTilingSpriteProps = [
4 | {
5 | prop: 'tilePosition',
6 | entry: {
7 | section: 'Tiling Sprite',
8 | options: { x: { label: 'x' }, y: { label: 'y' } },
9 | type: 'vector2',
10 | },
11 | },
12 | {
13 | prop: 'tileScale',
14 | entry: {
15 | section: 'Tiling Sprite',
16 | options: { x: { label: 'x' }, y: { label: 'y' } },
17 | type: 'vector2',
18 | },
19 | },
20 | { prop: 'tileRotation', entry: { section: 'Tiling Sprite', type: 'number' } },
21 | { prop: 'clampMargin', entry: { section: 'Tiling Sprite', type: 'number' } },
22 | ] as Properties[];
23 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/tilingSprite/tilingSpritePropertyExtension.ts:
--------------------------------------------------------------------------------
1 | import type { PropertiesEntry } from '@pixi/devtools';
2 | import type { Container } from 'pixi.js';
3 | import { PixiDevtools } from '../../../../pixi';
4 | import { isTilingSprite } from '../../../../utils/getPixiType';
5 | import type { DefaultPropertyExtension } from '../container/containerPropertyExtension';
6 | import { arrayToPoint, pointToArray } from '../utils/convertProp';
7 | import { getProps } from '../utils/getProps';
8 | import { v7TilingSpriteProps } from './v7TilingSpriteProps';
9 | import { v8TilingSpriteProps } from './v8TilingSpriteProps';
10 |
11 | const propertyValueExtractors = {
12 | tilePosition: pointToArray,
13 | tileScale: pointToArray,
14 | } as const;
15 |
16 | type PropertyValueExtractors = keyof typeof propertyValueExtractors;
17 |
18 | const propertyValueSetters: Record = {
19 | tilePosition: arrayToPoint,
20 | tileScale: arrayToPoint,
21 | };
22 |
23 | export const tilingSpritePropertyExtension: DefaultPropertyExtension = {
24 | extension: {
25 | type: 'sceneProperties',
26 | name: 'default-tiling-sprite-properties',
27 | priority: 0,
28 | },
29 | properties() {
30 | return getProps(v7TilingSpriteProps, v8TilingSpriteProps);
31 | },
32 | testNode(container) {
33 | return isTilingSprite(container, PixiDevtools.pixi);
34 | },
35 | testProp(prop: string) {
36 | return this.properties().some((p) => prop.startsWith(p.prop));
37 | },
38 | getProperties(container) {
39 | const activeProps = this.properties().reduce((result, property) => {
40 | const prop = property.prop;
41 | let value = container[prop as keyof Container];
42 |
43 | if (value != null && propertyValueExtractors[prop as PropertyValueExtractors]) {
44 | value = propertyValueExtractors[prop as PropertyValueExtractors](value);
45 | }
46 |
47 | if (value == null && property.allowUndefined !== true) {
48 | return result;
49 | }
50 |
51 | result.push({
52 | ...property,
53 | value,
54 | });
55 |
56 | return result;
57 | }, [] as PropertiesEntry[]);
58 |
59 | return activeProps;
60 | },
61 | setProperty(container, prop, value) {
62 | // set the property on the container
63 | if (propertyValueSetters[prop as PropertyValueExtractors]) {
64 | propertyValueSetters[prop as PropertyValueExtractors](container, prop, value);
65 | } else {
66 | (container as any)[prop] = value;
67 | }
68 | },
69 | };
70 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/tilingSprite/v7TilingSpriteProps.ts:
--------------------------------------------------------------------------------
1 | import type { Properties } from '@pixi/devtools';
2 | import { sharedTilingSpriteProps } from './sharedTilingSpriteProps';
3 |
4 | export const v7TilingSpriteProps = [
5 | ...sharedTilingSpriteProps,
6 | { value: null, prop: 'uvRespectAnchor', entry: { section: 'Transform', type: 'boolean' } },
7 | ] as Properties[];
8 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/tilingSprite/v8TilingSpriteProps.ts:
--------------------------------------------------------------------------------
1 | import type { Properties } from '@pixi/devtools';
2 | import { sharedTilingSpriteProps } from './sharedTilingSpriteProps';
3 |
4 | export const v8TilingSpriteProps = [...sharedTilingSpriteProps] as Properties[];
5 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/utils/convertProp.ts:
--------------------------------------------------------------------------------
1 | export const pointToArray = (value: any) => [value.x, value.y];
2 | export const boundsToArray = (value: any) => [value.x, value.y, value.width, value.height];
3 | export const matrixToArray = (value: any) => [value.a, value.b, value.c, value.d, value.tx, value.ty];
4 |
5 | export const arrayToPoint = (container: any, prop: string, value: any) => container[prop].set(value[0], value[1]);
6 | export const arrayToBounds = (container: any, prop: string, value: any) => {
7 | container[prop].x = value[0];
8 | container[prop].y = value[1];
9 | container[prop].width = value[2];
10 | container[prop].height = value[3];
11 | };
12 | export const arrayToMatrix = (container: any, prop: string, value: any) => container[prop].fromArray(value);
13 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/utils/getProps.ts:
--------------------------------------------------------------------------------
1 | import { PixiDevtools } from '../../../../pixi';
2 |
3 | export function getProps(v7: T, v8: K) {
4 | if (PixiDevtools.majorVersion === '7') {
5 | return v7;
6 | }
7 |
8 | return v8;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/view/viewPropertyExtension.ts:
--------------------------------------------------------------------------------
1 | import type { PropertiesEntry } from '@pixi/devtools';
2 | import type { AnimatedSprite } from 'pixi.js';
3 | import type { DefaultPropertyExtension } from '../container/containerPropertyExtension';
4 | import { arrayToPoint, pointToArray } from '../utils/convertProp';
5 | import { sharedViewProps } from './viewProps';
6 |
7 | export const viewPropertyExtension: DefaultPropertyExtension = {
8 | extension: {
9 | type: 'sceneProperties',
10 | name: 'default-view-properties',
11 | priority: 0,
12 | },
13 | properties() {
14 | return sharedViewProps;
15 | },
16 | testNode() {
17 | // any node could have these properties, so might as well always check
18 | return true;
19 | },
20 | testProp(prop: string) {
21 | // test if the property is in the list
22 | return this.properties().some((p) => prop.startsWith(p.prop));
23 | },
24 | getProperties(container: AnimatedSprite) {
25 | // get the properties from the container
26 | const activeProps = this.properties().reduce((result, property) => {
27 | const prop = property.prop;
28 | let value = container[prop as keyof AnimatedSprite];
29 |
30 | if (value != null && prop === 'anchor') {
31 | value = pointToArray(value);
32 | }
33 |
34 | if (value == null && property.allowUndefined !== true) {
35 | return result;
36 | }
37 |
38 | result.push({
39 | ...property,
40 | value,
41 | });
42 |
43 | return result;
44 | }, [] as PropertiesEntry[]);
45 |
46 | return activeProps;
47 | },
48 | setProperty(container: AnimatedSprite, prop, value) {
49 | if (prop === 'anchor') {
50 | arrayToPoint(container, prop, value);
51 | } else {
52 | (container as any)[prop] = value;
53 | }
54 | },
55 | };
56 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/extensions/view/viewProps.ts:
--------------------------------------------------------------------------------
1 | import type { Properties } from '@pixi/devtools';
2 |
3 | export const sharedViewProps = [
4 | {
5 | prop: 'anchor',
6 | entry: { section: 'Transform', options: { x: { label: 'x' }, y: { label: 'y' } }, type: 'vector2' },
7 | },
8 | { prop: 'roundPixels', entry: { section: 'Transform', type: 'boolean' } },
9 | {
10 | prop: 'batched',
11 | entry: {
12 | section: 'Rendering',
13 | options: { label: 'Batched', disabled: true },
14 | type: 'boolean',
15 | },
16 | },
17 | ] as Properties[];
18 |
--------------------------------------------------------------------------------
/packages/backend/src/scene/tree/properties.ts:
--------------------------------------------------------------------------------
1 | import type { PropertiesExtension } from '@pixi/devtools';
2 | import { extensions } from '../../extensions/Extensions';
3 | import { PixiHandler } from '../../handler';
4 |
5 | export class Properties extends PixiHandler {
6 | public static extensions: PropertiesExtension[] = [];
7 |
8 | private _extensions!: PropertiesExtension[];
9 |
10 | public override init() {
11 | this._extensions = Properties.extensions;
12 | }
13 |
14 | public setValue(prop: string, value: any) {
15 | const selectedNode = this._devtool.scene.tree.selectedNode;
16 |
17 | if (!selectedNode) return;
18 |
19 | this._extensions.forEach((plugin) => {
20 | if (plugin.testNode(selectedNode) && plugin.testProp(prop)) {
21 | plugin.setProperty(selectedNode, prop, value);
22 | }
23 | });
24 | }
25 |
26 | public getActiveProps() {
27 | const selectedNode = this._devtool.scene.tree.selectedNode;
28 | if (!selectedNode) return;
29 |
30 | if (selectedNode.__devtoolLocked) return;
31 |
32 | const activeProps = this._extensions.reduce(
33 | (result, plugin) => {
34 | if (plugin.testNode(selectedNode)) {
35 | result.push(...plugin.getProperties(selectedNode));
36 | }
37 | return result;
38 | },
39 | [] as ReturnType,
40 | );
41 |
42 | return activeProps || [];
43 | }
44 | }
45 |
46 | extensions.handleByList('sceneProperties', Properties.extensions);
47 |
--------------------------------------------------------------------------------
/packages/backend/src/utils/loop.ts:
--------------------------------------------------------------------------------
1 | import type { Container } from 'pixi.js';
2 |
3 | interface LoopOptions {
4 | loop: (container: Container, parent: Container) => void;
5 | test?: (container: Container, parent: Container) => boolean | 'children';
6 | container: Container;
7 | }
8 |
9 | export function loop(options: LoopOptions) {
10 | const { container, ...rest } = options;
11 |
12 | loopRecursive(container, rest);
13 | }
14 |
15 | function loopRecursive(container: Container, opts: Omit) {
16 | const { loop, test } = opts;
17 |
18 | if (!container) {
19 | return;
20 | }
21 |
22 | const testResult = test?.(container, container.parent) ?? true;
23 |
24 | if (testResult === false) {
25 | return;
26 | }
27 |
28 | loop(container, container.parent);
29 |
30 | if (container.children.length === 0 || testResult === 'children') {
31 | return;
32 | }
33 |
34 | for (let i = 0; i < container.children.length; i++) {
35 | loopRecursive(container.children[i], opts);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/backend/src/utils/poller.ts:
--------------------------------------------------------------------------------
1 | import { DevtoolMessage } from '@devtool/frontend/types';
2 | import { PixiDevtools } from '../pixi';
3 |
4 | // Constants
5 | const MAX_TRIES = 10;
6 | const POLLING_INTERVAL_MS = 1000;
7 |
8 | let pixiPollingInterval: number | undefined;
9 | let tryCount = 0;
10 |
11 | // Function to start polling for PixiJS
12 | export function pollPixi(foundCallback: () => void, loopCallback?: () => void, stopCallback?: () => void) {
13 | // Start a new interval
14 | pixiPollingInterval = window.setInterval(() => {
15 | // If we've tried more than the max tries, stop polling
16 | if (tryCount > MAX_TRIES) {
17 | stopPolling();
18 | if (stopCallback) {
19 | stopCallback();
20 | }
21 | return;
22 | }
23 |
24 | // If a loop callback is provided, call it
25 | if (loopCallback) {
26 | loopCallback();
27 | }
28 | // Try to detect Pixi
29 | tryDetectPixi(foundCallback);
30 |
31 | // Increment the try count
32 | tryCount += 1;
33 | }, POLLING_INTERVAL_MS);
34 |
35 | // Return the interval ID
36 | return pixiPollingInterval;
37 | }
38 |
39 | // Function to stop polling
40 | function stopPolling() {
41 | if (pixiPollingInterval) {
42 | clearInterval(pixiPollingInterval);
43 | }
44 | }
45 |
46 | // Function to try to detect Pixi
47 | function tryDetectPixi(foundCallback: () => void) {
48 | try {
49 | const pixiDetectionResult = PixiDevtools.isPixiActive;
50 |
51 | // If Pixi is active, stop polling and post a message
52 | if (pixiDetectionResult === DevtoolMessage.active) {
53 | stopPolling();
54 | foundCallback();
55 | } else {
56 | window.postMessage({ method: pixiDetectionResult, data: JSON.stringify({}) }, '*');
57 | }
58 | } catch (error) {
59 | // If an error occurred, stop polling
60 | stopPolling();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/packages/backend/src/utils/throttle.ts:
--------------------------------------------------------------------------------
1 | export class Throttle {
2 | private lastUpdateTime: number;
3 |
4 | constructor() {
5 | this.lastUpdateTime = 0;
6 | }
7 |
8 | shouldExecute(interval: number): boolean {
9 | const currentTime = Date.now();
10 | if (currentTime - this.lastUpdateTime >= interval) {
11 | this.lastUpdateTime = currentTime;
12 | return true;
13 | }
14 | return false;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "paths": {
5 | "@devtool/frontend/*": ["packages/frontend/src/*"],
6 | "@pixi/devtools": ["packages/api/src/index.ts"]
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/devtool-chrome/assets/contentStyle.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/devtool-chrome/assets/contentStyle.css
--------------------------------------------------------------------------------
/packages/devtool-chrome/assets/pixi-icon-active-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/devtool-chrome/assets/pixi-icon-active-128.png
--------------------------------------------------------------------------------
/packages/devtool-chrome/assets/pixi-icon-active-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/devtool-chrome/assets/pixi-icon-active-16.png
--------------------------------------------------------------------------------
/packages/devtool-chrome/assets/pixi-icon-active-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/devtool-chrome/assets/pixi-icon-active-48.png
--------------------------------------------------------------------------------
/packages/devtool-chrome/assets/pixi-icon-inactive-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/devtool-chrome/assets/pixi-icon-inactive-128.png
--------------------------------------------------------------------------------
/packages/devtool-chrome/assets/pixi-icon-inactive-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/devtool-chrome/assets/pixi-icon-inactive-16.png
--------------------------------------------------------------------------------
/packages/devtool-chrome/assets/pixi-icon-inactive-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/devtool-chrome/assets/pixi-icon-inactive-48.png
--------------------------------------------------------------------------------
/packages/devtool-chrome/manifest.dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "DEV: PixiJS DevTools",
3 | "action": {
4 | "default_icon": {
5 | "16": "pixi-icon-inactive-16.png",
6 | "48": "pixi-icon-inactive-48.png",
7 | "128": "pixi-icon-inactive-128.png"
8 | }
9 | },
10 | "content_scripts": [
11 | {
12 | "matches": ["http://*/*", "https://*/*", ""],
13 | "js": ["content/index.ts"],
14 | "css": ["contentStyle.css"],
15 | "run_at": "document_start"
16 | }
17 | ],
18 | "background": {
19 | "service_worker": "background/index.ts",
20 | "type": "module"
21 | },
22 | "icons": {
23 | "16": "pixi-icon-active-16.png",
24 | "48": "pixi-icon-active-48.png",
25 | "128": "pixi-icon-active-128.png"
26 | },
27 | "permissions": ["activeTab"],
28 | "devtools_page": "devtools/pixi-index.html",
29 | "web_accessible_resources": [
30 | {
31 | "resources": ["contentStyle.css", "pixi-icon-active-128.png", "pixi-icon-inactive-48.png"],
32 | "matches": [""]
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/packages/devtool-chrome/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "PixiJS DevTools",
4 | "description": "DevTools for PixiJS",
5 | "action": {
6 | "default_icon": {
7 | "16": "pixi-icon-inactive-16.png",
8 | "48": "pixi-icon-inactive-48.png",
9 | "128": "pixi-icon-inactive-128.png"
10 | }
11 | },
12 | "content_scripts": [
13 | {
14 | "matches": ["http://*/*", "https://*/*", ""],
15 | "js": ["content/index.ts"],
16 | "css": ["contentStyle.css"],
17 | "run_at": "document_start"
18 | }
19 | ],
20 | "background": {
21 | "service_worker": "background/index.ts",
22 | "type": "module"
23 | },
24 | "icons": {
25 | "16": "pixi-icon-active-16.png",
26 | "48": "pixi-icon-active-48.png",
27 | "128": "pixi-icon-active-128.png"
28 | },
29 | "permissions": ["activeTab"],
30 | "devtools_page": "devtools/pixi-index.html"
31 | }
32 |
--------------------------------------------------------------------------------
/packages/devtool-chrome/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@devtool/chrome",
3 | "private": true,
4 | "version": "2.2.1",
5 | "type": "module",
6 | "scripts": {
7 | "build": "run-s build:*",
8 | "build:chrome": "vite build --config vite.chrome.config.ts",
9 | "build:inject": "vite build --config vite.inject.config.ts",
10 | "watch": "run-p watch:*",
11 | "watch:chrome": "vite dev --config vite.chrome.config.ts",
12 | "watch:inject": "node -e \"setTimeout(() => process.exit(0), 5000)\" && vite build --config vite.inject.config.ts --watch --mode development",
13 | "start": "npm run watch"
14 | },
15 | "dependencies": {
16 | "@devtool/frontend": "*",
17 | "@devtool/backend": "*"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/devtool-chrome/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/packages/devtool-chrome/src/content/index.ts:
--------------------------------------------------------------------------------
1 | import { convertPostMessage, convertPostMessageData } from '../messageUtils';
2 |
3 | function injectScript(file_path: string, tag: string) {
4 | const node = document.getElementsByTagName(tag)[0];
5 |
6 | if (!node) {
7 | console.error('[Pixi Devtool] Failed to inject script, please submit an issue');
8 | return;
9 | }
10 |
11 | const script = document.createElement('script');
12 | script.setAttribute('type', 'text/javascript');
13 | script.setAttribute('src', file_path);
14 | node.appendChild(script);
15 | }
16 |
17 | if (document.readyState === 'loading') {
18 | // Loading hasn't finished yet
19 | document.addEventListener('DOMContentLoaded', function () {
20 | injectScript(chrome.runtime.getURL('inject/index.js'), 'body');
21 | });
22 | } else {
23 | // `DOMContentLoaded` has already fired
24 | injectScript(chrome.runtime.getURL('inject/index.js'), 'body');
25 | }
26 |
27 | window.addEventListener(
28 | 'message',
29 | (event) => {
30 | // We only accept messages from ourselves
31 | if (event.source !== window || !event.data.method) {
32 | return;
33 | }
34 |
35 | const converted = convertPostMessageData(event.data);
36 |
37 | if (!converted.method.startsWith('pixi-')) return;
38 | // pass the message to the background
39 | const message = convertPostMessage(converted.method, converted.data);
40 | chrome.runtime.sendMessage(message);
41 | },
42 | false,
43 | );
44 |
45 | chrome.runtime.onMessage.addListener(function (message) {
46 | if (message.method === 'panelClosed') {
47 | injectScript(chrome.runtime.getURL('inject/index2.js'), 'body');
48 | }
49 | });
50 |
--------------------------------------------------------------------------------
/packages/devtool-chrome/src/devtools/devtools.ts:
--------------------------------------------------------------------------------
1 | import { DevtoolMessage } from '@devtool/frontend/types';
2 | import { convertPostMessage } from '../messageUtils';
3 |
4 | chrome.devtools.panels.create(
5 | import.meta.env.DEV ? 'Dev: PixiJS DevTools' : 'PixiJS DevTools',
6 | 'pixi-icon-active-128.png',
7 | 'devtools/panel/panel.html',
8 | (panel) => {
9 | const tabId = chrome.devtools.inspectedWindow.tabId;
10 |
11 | panel.onShown.addListener(() => {
12 | const message = convertPostMessage(DevtoolMessage.panelShown, {});
13 | chrome.runtime.sendMessage({ ...message, tabId });
14 | });
15 |
16 | panel.onHidden.addListener(() => {
17 | const message = convertPostMessage(DevtoolMessage.panelHidden, {});
18 | chrome.runtime.sendMessage({ ...message, tabId });
19 | });
20 | },
21 | );
22 |
--------------------------------------------------------------------------------
/packages/devtool-chrome/src/devtools/panel/panel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Dev Tools Panel
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/devtool-chrome/src/devtools/panel/panel.tsx:
--------------------------------------------------------------------------------
1 | import App from '@devtool/frontend/App';
2 | import type { BridgeFn } from '@devtool/frontend/lib/utils';
3 |
4 | import { createRoot } from 'react-dom/client';
5 |
6 | /**
7 | * Thanks pixi-inspector for this snippet
8 | * https://github.com/bfanger/pixi-inspector
9 | */
10 | const bridge: BridgeFn = (code: string) =>
11 | new Promise((resolve, reject) => {
12 | chrome.devtools.inspectedWindow.eval(code, (result, err) => {
13 | if (err) {
14 | if (err instanceof Error) {
15 | reject(err);
16 | }
17 | reject(new Error(err.value || err.description || err.code) + `\n${code}`);
18 | }
19 | resolve(result as any);
20 | });
21 | });
22 |
23 | const container = document.getElementById('app-container');
24 | const root = createRoot(container!);
25 | root.render( );
26 |
27 | if (import.meta.env.DEV) {
28 | new EventSource('http://localhost:10808').addEventListener('change', () => {
29 | bridge('window.location.reload()');
30 | window.location.reload();
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/packages/devtool-chrome/src/devtools/pixi-index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/packages/devtool-chrome/src/inject/close.ts:
--------------------------------------------------------------------------------
1 | window.__PIXI_DEVTOOLS_WRAPPER__?.scene.overlay.enableHighlight(false);
2 | window.__PIXI_DEVTOOLS_WRAPPER__?.scene.overlay.enablePicker(false);
3 | window.__PIXI_DEVTOOLS_WRAPPER__?.reset();
4 |
--------------------------------------------------------------------------------
/packages/devtool-chrome/src/inject/index.ts:
--------------------------------------------------------------------------------
1 | // Note: this file is compiled to `dist/content-inject/index.js` and is used by the content script
2 |
3 | import { pollPixi } from '@devtool/backend/utils/poller';
4 | import { DevtoolMessage } from '@devtool/frontend/types';
5 | import { convertPostMessage } from '../messageUtils';
6 | import type { Application, Renderer } from 'pixi.js';
7 |
8 | function attach() {
9 | window.postMessage(convertPostMessage(DevtoolMessage.active, {}), '*');
10 | }
11 | function injectIFrames() {
12 | if (window.frames) {
13 | for (let i = 0; i < window.frames.length; i += 1) {
14 | try {
15 | const frame = window.frames[i] as Window & typeof globalThis;
16 | frame.__PIXI_APP_INIT__ = init;
17 | frame.__PIXI_RENDERER_INIT__ = init;
18 | } catch (_) {
19 | // access to iframe was denied
20 | }
21 | }
22 | }
23 | window.__PIXI_APP_INIT__ = init;
24 | window.__PIXI_RENDERER_INIT__ = init;
25 |
26 | return null;
27 | }
28 | function init(arg: Application | Renderer, version?: string) {
29 | const stage = (arg as Application).stage;
30 | const renderer = stage ? (arg as Application).renderer : (arg as Renderer);
31 | window.__PIXI_DEVTOOLS__ = {
32 | ...window.__PIXI_DEVTOOLS__,
33 | renderer,
34 | stage,
35 | version,
36 | };
37 | window.__PIXI_DEVTOOLS_WRAPPER__?.reset();
38 | }
39 |
40 | // try injecting iframes using requestAnimationFrame until poller is stopped
41 | const inject = () => {
42 | injectIFrames();
43 | requestAnimationFrame(inject);
44 | };
45 |
46 | inject();
47 |
48 | pollPixi(attach);
49 |
--------------------------------------------------------------------------------
/packages/devtool-chrome/src/messageUtils.ts:
--------------------------------------------------------------------------------
1 | import type { DevtoolMessage } from '@devtool/frontend/types';
2 |
3 | /**
4 | * Standard message format
5 | * @param method - The message type
6 | * @param data - The message data
7 | * @returns
8 | */
9 | export function convertPostMessage(method: DevtoolMessage, data: unknown) {
10 | return { method, data: JSON.stringify(data) };
11 | }
12 |
13 | /**
14 | * Convert the message data to a standard format
15 | * @param a - The original message
16 | */
17 | export function convertPostMessageData(a: ReturnType) {
18 | let data: string | object = a.data;
19 |
20 | if (typeof data === 'string') {
21 | try {
22 | data = JSON.parse(data);
23 | } catch (error) {
24 | data = {};
25 | }
26 | }
27 | return {
28 | method: a.method,
29 | data,
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/packages/devtool-chrome/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | import theme from '../frontend/tailwind.config.js';
3 | theme.content = [
4 | '../frontend/src/pages/**/*.{ts,tsx}',
5 | '../frontend/src/components/**/*.{ts,tsx}',
6 | '../frontend/src/app/**/*.{ts,tsx}',
7 | '../frontend/src/**/*.{ts,tsx}',
8 | ];
9 | export default theme;
10 |
--------------------------------------------------------------------------------
/packages/devtool-chrome/vite.chrome.config.ts:
--------------------------------------------------------------------------------
1 | import type { ManifestV3Export } from '@crxjs/vite-plugin';
2 | import { crx } from '@crxjs/vite-plugin';
3 | import react from '@vitejs/plugin-react-swc';
4 | import { defineConfig } from 'vite';
5 | import fs from 'fs';
6 | import path, { resolve } from 'node:path';
7 |
8 | import pkg from './package.json';
9 | import devManifest from './manifest.dev.json';
10 | import manifest from './manifest.json';
11 |
12 | export default defineConfig((config) => {
13 | const isDev = config.mode === 'development';
14 | const publicDir = resolve(__dirname, './assets');
15 | const outDir = resolve(__dirname, isDev ? 'dist/chrome-dev' : 'dist/chrome');
16 | const extensionManifest = {
17 | ...manifest,
18 | ...(isDev ? devManifest : ({} as ManifestV3Export)),
19 | name: isDev ? `DEV: ${manifest.name}` : manifest.name,
20 | version: pkg.version,
21 | };
22 |
23 | return {
24 | root: resolve(__dirname, 'src/'),
25 | resolve: {
26 | alias: {
27 | '@devtool/frontend': path.resolve(process.cwd(), '../../packages/frontend/src/'),
28 | '@devtool/backend': path.resolve(process.cwd(), '../../packages/backend/src/'),
29 | },
30 | },
31 | plugins: [
32 | react(),
33 | crx({
34 | manifest: extensionManifest as ManifestV3Export,
35 | contentScripts: {
36 | injectCss: true,
37 | },
38 | }),
39 | {
40 | name: 'manifest-plugin',
41 | enforce: 'post',
42 | writeBundle() {
43 | const manifestPath = path.resolve(__dirname, 'dist/chrome', 'manifest.json');
44 | if (fs.existsSync(manifestPath)) {
45 | const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
46 | const resource = manifest.web_accessible_resources[0];
47 | resource.resources.push('inject/index.js');
48 | resource.resources.push('inject/index2.js');
49 | fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
50 | }
51 | },
52 | },
53 | ],
54 | publicDir,
55 | build: {
56 | outDir,
57 | sourcemap: isDev,
58 | rollupOptions: {
59 | input: {
60 | panel: resolve(__dirname, 'src/devtools/panel/panel.html'),
61 | },
62 | },
63 | },
64 | server: {
65 | port: 10808,
66 | },
67 | };
68 | });
69 |
--------------------------------------------------------------------------------
/packages/devtool-chrome/vite.inject.config.ts:
--------------------------------------------------------------------------------
1 | import path, { resolve } from 'node:path';
2 | import { defineConfig } from 'vite';
3 |
4 | // the crx plugin doesn't seem to work with additional files, so we'll just build the injection library here
5 | export default defineConfig((config) => {
6 | const isDev = config.mode === 'development';
7 | const outDir = isDev ? 'chrome-dev' : 'chrome';
8 | return {
9 | resolve: {
10 | alias: {
11 | '@devtool/frontend': path.resolve(process.cwd(), '../../packages/frontend/src/'),
12 | '@devtool/backend': path.resolve(process.cwd(), '../../packages/backend/src/'),
13 | },
14 | },
15 | root: resolve(__dirname, 'src/'),
16 | plugins: [
17 | {
18 | name: 'wrap-in-iife',
19 | generateBundle(_outputOptions, bundle) {
20 | Object.keys(bundle).forEach((fileName) => {
21 | const file = bundle[fileName];
22 | if (fileName.slice(-3) === '.js' && 'code' in file) {
23 | file.code = `(() => {\n${file.code}})()`;
24 | }
25 | });
26 | },
27 | },
28 | ],
29 | build: {
30 | lib: {
31 | entry: ['inject/index.ts', 'inject/close.ts'],
32 | fileName: 'index',
33 | formats: ['es'],
34 | },
35 | target: 'es2020',
36 | outDir: resolve(__dirname, `dist/${outDir}/inject`),
37 | },
38 | };
39 | });
40 |
--------------------------------------------------------------------------------
/packages/devtool-local/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pixi Devtool - Local
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/packages/devtool-local/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@devtool/local",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "build": "vite build",
8 | "watch": "vite dev",
9 | "start": "npm run watch"
10 | },
11 | "dependencies": {
12 | "@devtool/frontend": "*",
13 | "@devtool/backend": "*",
14 | "@pixi/devtools": "*"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/devtool-local/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/packages/devtool-local/src/main.tsx:
--------------------------------------------------------------------------------
1 | import App from '@devtool/frontend/App';
2 | import type { BridgeFn } from '@devtool/frontend/lib/utils';
3 | import React from 'react';
4 | import ReactDOM from 'react-dom/client';
5 | import { PixiDevtools } from '@devtool/backend/pixi';
6 | import scene from './scene.ts';
7 | import * as PIXI from 'pixi.js';
8 |
9 | (async () => {
10 | // wait for the app to be ready
11 | await new Promise((resolve) => setTimeout(resolve, 1000));
12 |
13 | const canvas = document.getElementById('canvas') as HTMLCanvasElement;
14 |
15 | const app = await scene(canvas);
16 |
17 | window.__PIXI_DEVTOOLS__ = {
18 | pixi: PIXI,
19 | app,
20 | };
21 |
22 | const renderer = PixiDevtools.renderer;
23 |
24 | if (renderer) {
25 | renderer.render = new Proxy(renderer.render, {
26 | apply(target, thisArg, ...args) {
27 | PixiDevtools.update();
28 | messageListeners.forEach((listener) =>
29 | listener({
30 | method: 'pixi-state-update',
31 | data: JSON.stringify(PixiDevtools.state),
32 | }),
33 | );
34 | // @ts-expect-error - TODO: fix this type
35 | return target.apply(thisArg, ...args);
36 | },
37 | });
38 | }
39 | })();
40 |
41 | const messageListeners: ((message: unknown) => void)[] = [];
42 | const mockChrome = {
43 | runtime: {
44 | connect: () => ({
45 | postMessage: () => {},
46 | onMessage: {
47 | addListener: (listner: (message: unknown) => void) => {
48 | messageListeners.push(listner);
49 | },
50 | },
51 | }),
52 | },
53 | devtools: {
54 | inspectedWindow: {
55 | tabId: 0,
56 | eval: (code: string, cb: () => void) => {
57 | eval(code);
58 | cb();
59 | },
60 | },
61 | },
62 | } as unknown as typeof chrome;
63 |
64 | const mockBridge: BridgeFn = (code): Promise => {
65 | eval(code);
66 | return Promise.resolve();
67 | };
68 |
69 | ReactDOM.createRoot(document.getElementById('root')!).render(
70 |
71 |
77 | ,
78 | );
79 |
--------------------------------------------------------------------------------
/packages/devtool-local/src/scene.ts:
--------------------------------------------------------------------------------
1 | import { Application, Assets, Container, Sprite, Rectangle } from 'pixi.js';
2 |
3 | export default async function buildScene(canvas: HTMLCanvasElement) {
4 | // Create a new application
5 | const app = new Application();
6 |
7 | // Initialize the application
8 | await app.init({ background: '#1099bb', canvas });
9 |
10 | // Create and add a container to the stage
11 | const container = new Container();
12 |
13 | app.stage.addChild(container);
14 |
15 | // Load the bunny texture
16 | const texture = await Assets.load('https://pixijs.com/assets/bunny.png');
17 |
18 | // Create a 5x5 grid of bunnies in the container
19 | for (let i = 0; i < 25; i++) {
20 | const bunny = new Sprite(texture);
21 |
22 | bunny.x = (i % 5) * 40;
23 | bunny.y = Math.floor(i / 5) * 40;
24 | bunny.label = `Bunny ${i}`;
25 | bunny.filterArea = new Rectangle(0, 0, 40, 40);
26 | // bunny.boundsArea = new PIXI.Rectangle(0, 0, 40, 40);
27 | container.addChild(bunny);
28 | }
29 |
30 | // Move the container to the center
31 | container.x = app.screen.width / 2;
32 | container.y = app.screen.height / 2;
33 |
34 | // Center the bunny sprites in local container coordinates
35 | container.pivot.x = container.width / 2;
36 | container.pivot.y = container.height / 2;
37 |
38 | // Listen for animate update
39 | app.ticker.add((time) => {
40 | // Continuously rotate the container!
41 | // * use delta to create frame-independent transform *
42 | container.rotation -= 0.01 * time.deltaTime;
43 | });
44 |
45 | // add two buttons to add and remove bunnies
46 | const addButton = document.createElement('button');
47 | addButton.textContent = 'Add Bunny';
48 | // position over top of everything else
49 | addButton.style.position = 'absolute';
50 | addButton.style.top = '10px';
51 | addButton.style.left = '10px';
52 |
53 | // addButton.onclick = () => {
54 | // const bunny = new Sprite(texture);
55 | // const i = container.children.length;
56 | // bunny.x = (i % 5) * 40;
57 | // bunny.y = Math.floor(i / 5) * 40;
58 | // container.addChild(bunny);
59 | // };
60 |
61 | // document.body.appendChild(addButton);
62 |
63 | // const removeButton = document.createElement('button');
64 | // removeButton.textContent = 'Remove Bunny';
65 | // // position over top of everything else
66 | // removeButton.style.position = 'absolute';
67 | // removeButton.style.top = '10px';
68 | // removeButton.style.left = '100px';
69 | // removeButton.onclick = () => {
70 | // container.removeChild(container.children[container.children.length - 1]);
71 | // };
72 |
73 | // document.body.appendChild(removeButton);
74 |
75 | return app;
76 | }
77 |
--------------------------------------------------------------------------------
/packages/devtool-local/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | import theme from '../frontend/tailwind.config.js';
3 | theme.content = [
4 | '../frontend/src/pages/**/*.{ts,tsx}',
5 | '../frontend/src/components/**/*.{ts,tsx}',
6 | '../frontend/src/app/**/*.{ts,tsx}',
7 | '../frontend/src/**/*.{ts,tsx}',
8 | ];
9 | export default theme;
10 |
--------------------------------------------------------------------------------
/packages/devtool-local/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import react from '@vitejs/plugin-react';
3 | import { defineConfig } from 'vite';
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | resolve: {
8 | alias: {
9 | '@devtool/frontend': path.resolve(process.cwd(), '../../packages/frontend/src/'),
10 | '@devtool/backend': path.resolve(process.cwd(), '../../packages/backend/src/'),
11 | '@pixi/devtool': path.resolve(process.cwd(), '../../packages/api/src/'),
12 | },
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/packages/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | /node_modules
3 |
4 | # Production
5 | /build
6 |
7 | # Generated files
8 | .docusaurus
9 | .cache-loader
10 |
11 | # Misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
--------------------------------------------------------------------------------
/packages/docs/README.md:
--------------------------------------------------------------------------------
1 | # Website
2 |
3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
4 |
5 | ### Installation
6 |
7 | ```
8 | $ yarn
9 | ```
10 |
11 | ### Local Development
12 |
13 | ```
14 | $ yarn start
15 | ```
16 |
17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
18 |
19 | ### Build
20 |
21 | ```
22 | $ yarn build
23 | ```
24 |
25 | This command generates static content into the `build` directory and can be served using any static contents hosting service.
26 |
27 | ### Deployment
28 |
29 | Using SSH:
30 |
31 | ```
32 | $ USE_SSH=true yarn deploy
33 | ```
34 |
35 | Not using SSH:
36 |
37 | ```
38 | $ GIT_USER= yarn deploy
39 | ```
40 |
41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
42 |
--------------------------------------------------------------------------------
/packages/docs/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/docs/docs/guide/faq.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 3
3 | ---
4 |
5 | # F.A.Q
6 |
7 | ## The PixiJS Devtools don't show up
8 |
9 | Here are some troubleshooting steps to help you if you don't the PixiJS devtools in your browser:
10 |
11 | * Check if the extension is installed and enabled:
12 |
13 | Open the Chrome extensions page by typing `chrome://extensions` in the address bar. Make sure the PixiJS Devtools extension is installed and enabled.
14 | If it's not installed, please read the [installation guide](/docs/guide/installation).
15 |
16 | * Try closing the devtools pane, refreshing the page and opening the devtools pane again**.
17 | * Try restarting the browser or the computer.
18 | * If you have multiple pages with the PixiJS Devtools open, try closing all of them and opening a new one.
19 | * If you have multiple versions of the devtools installed, it's recommended to disable/remove the others. n
20 | * Look for errors in the browser Console.
21 |
22 | If you see any errors, please report them in the [GitHub issues](https://github.com/pixijs/devtools/issues).
23 |
24 | ---
25 |
26 | ## Scene isn't updating in the Devtools
27 |
28 | If the scene isn't updating in the devtools, try the following steps:
29 |
30 | * Close the devtools pane.
31 | * Refresh the page.
32 | * Open the devtools pane again.
33 |
34 | If the scene still isn't updating, try looking for errors in the console:
35 |
36 | * Open the browser console, you can do this by pressing `ESC`
37 | * Look for errors in the devtools Console.
38 | You can open the devtools console by right-clicking on the devtools pane and selecting "Inspect".
39 |
40 |
41 |
42 | How to open the devtools console
43 |
44 |
45 |
46 |
47 |
48 | If you see any errors, please report them in the [GitHub issues](https://github.com/pixijs/devtools/issues).
49 |
50 | ---
51 |
52 | ## Property is not displayed
53 |
54 | If a property is not displayed in the devtools, it might be because the property is not set on the object.
55 |
56 | For example, the `filterArea` property is not displayed in the devtool until it's set on the object in your code.
57 | To get the property to display, you can set it on the object like this:
58 |
59 | ```js
60 | const sprite = new Sprite(texture);
61 | sprite.filterArea = new Rectangle(0, 0, 100, 100);
62 | ```
63 |
64 | If you think a property should be displayed, please report it in the [GitHub issues](https://github.com/pixijs/devtools/issues).
65 |
--------------------------------------------------------------------------------
/packages/docs/docs/guide/features/_category_.yml:
--------------------------------------------------------------------------------
1 | label: Features
2 | position: 1
3 | collapsed: false
4 |
--------------------------------------------------------------------------------
/packages/docs/docs/guide/features/assets.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | title: Assets
4 | ---
5 |
6 | import React from 'react';
7 | import { useColorMode } from '@docusaurus/theme-common';
8 |
9 | export const Gif = ({ src, alt }) => {
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | # Asset Panel
18 |
19 | The Asset Panel in PixiJS provides a detailed view of all assets currently utilized by your application. This tool is invaluable for monitoring GPU memory usage
20 | and identifying which textures are loaded onto the GPU.
21 |
22 | By visualizing your assets, the Asset Panel becomes a powerful resource for debugging and optimizing your PixiJS application. It helps you detect and remove
23 | unused assets that are still loaded on the GPU, ensuring efficient memory usage and improved performance.
24 |
25 | ## Texture Inspection
26 |
27 | The Asset Panel displays all textures currently loaded onto the GPU. You can view the texture's dimensions, format, and memory usage. The panel also provides
28 | a preview of the texture, making it easy to identify the asset.
29 |
30 |
31 |
32 | ## Texture Search
33 |
34 | The Asset Panel includes a search feature that allows you to quickly find a specific texture.
35 |
36 |
37 | ## Texture Sorting
38 |
39 | You can sort the textures in the Asset Panel by various criteria, such as memory usage or dimensions. This feature helps you identify large textures that may
40 | be impacting your application's performance.
41 |
42 |
43 |
--------------------------------------------------------------------------------
/packages/docs/docs/plugin/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 0
3 | ---
4 |
5 | # Getting Started
6 |
7 | This guide will help you create your own extensions for the devtools.
8 |
9 | :::warning
10 | The devtools are still in development and the API may change in the future.
11 |
12 | We are also still improving the documentation and adding more examples.
13 | :::
14 |
15 | ## Extension Types
16 |
17 | There are several types of extensions that you can create for the devtools that allow you to extend the functionality of the devtool:
18 |
19 | - [Properties](properties.md) - Add properties to be displayed in the tree.
20 | - [Overlay](overlay.md) - Highlight the bounds of a PixiJS object in the scene.
21 | - [Stats](stats.md) - Track stats for a node in the scene.
22 | - [Tree](tree.md) - Customise nodes in the tree.
23 |
24 | ## Adding an Extension
25 |
26 | To add an extension to the devtools, you need to add it to the `extensions` array in the `devtools` options when you initialise the devtools.
27 |
28 | ```ts
29 | import { initDevtools } from '@pixi/devtools';
30 |
31 | initDevtools({
32 | ...
33 | extensions: [
34 | // Add your extension here
35 | ],
36 | });
37 | ```
38 |
39 | or adding it directly to the window:
40 |
41 | ```js
42 | window.__PIXI_DEVTOOLS__ = {
43 | ...
44 | extensions: [
45 | // Add your extension here
46 | ],
47 | };
48 | ```
49 | :::note
50 | At this time, the devtool does not support adding extensions after the devtool has been initialised.
51 | :::
52 |
--------------------------------------------------------------------------------
/packages/docs/docs/plugin/overlay.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Overlay
3 | ---
4 | # Overlay Extension
5 |
6 | The overlay extension allows you to highlight the bounds of a PixiJS object in the scene. This is useful for debugging layout and alignment issues.
7 | You can customise the style of the overlay for selected and hovered nodes, as well as define the bounds of the node.
8 |
9 | Since you can change the styling and bounds per node, you can use this extension to create custom overlays for different types of objects.
10 |
11 | ```ts
12 | export interface OverlayExtension {
13 | /**
14 | * The metadata for the extension.
15 | */
16 | extension: ExtensionMetadata;
17 | /**
18 | * Get the css style for the selected highlight overlay.
19 | * @param node The selected node.
20 | */
21 | getSelectedStyle?(node: Container | null): Partial;
22 | /**
23 | * Get the css style for the hover highlight overlay.
24 | * @param node The hovered node.
25 | */
26 | getHoverStyle?(node: Container | null): Partial;
27 | /**
28 | * Get the global position of the node.
29 | * @param node The currently selected node to get the global position of.
30 | */
31 | getGlobalBounds?(node: Container): { x: number; y: number; width: number; height: number };
32 | }
33 | ```
34 |
35 | #### Example
36 |
37 | ```ts
38 | import { Container, Sprite, Text, Rectangle } from 'pixi.js';
39 | import type { ExtensionMetadata } from '@pixi-spine/devtools';
40 |
41 | export const overlay: OverlayExtension = {
42 | extension: {
43 | type: 'overlay',
44 | name: 'custom-overlay',
45 | },
46 | getSelectedStyle(node: Container | null) {
47 | const options = {} as Partial;
48 |
49 | if (node instanceof Sprite) {
50 | return {
51 | border: '2px solid #ff0000',
52 | };
53 | } else if (node instanceof Container) {
54 | return {
55 | border: '1px solid #00ff00',
56 | };
57 | }
58 |
59 | return {
60 | border: '2px solid #ff0000',
61 | };
62 | },
63 | getHoverStyle(node: Container | null) {
64 | return {
65 | border: '2px solid #00ff00',
66 | };
67 | },
68 | getGlobalBounds(node: Container) {
69 | if (node instanceof Text) {
70 | // Custom bounds for text objects
71 | const bounds = node.getBounds();
72 | return new Rectangle(bounds.x, bounds.y, bounds.width + 10, bounds.height + 10);
73 | }
74 | // Default bounds
75 | return node.getBounds();
76 | },
77 | };
78 | ```
79 |
--------------------------------------------------------------------------------
/packages/docs/docs/plugin/stats.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Stats
3 | ---
4 | # Stats Extension
5 |
6 | The stats extension allows you to provided valuable insights into the current scene by displaying various metrics, such as the number of specific nodes or any other data you wish to track. This tool enhances your ability to monitor and optimize your PixiJS application.
7 |
8 | ```ts
9 | export interface StatsExtension {
10 | /**
11 | * The metadata for the extension.
12 | */
13 | extension: ExtensionMetadata;
14 | /**
15 | * Track some stats for a node.
16 | * This function is called on every node in the scene.
17 | * @param container The current node
18 | * @param state The current stats for the scene.
19 | */
20 | track: (container: Container, state: Record) => void;
21 | /**
22 | * Get the keys of what ypu are tracking.
23 | * This is used to warn the user if the same key is being tracked by multiple extensions.
24 | */
25 | getKeys: () => string[];
26 | }
27 | ```
28 |
29 | #### Example
30 |
31 | ```ts
32 | import { Sprite } from 'pixi.js';
33 | import type { Container } from 'pixi.js';
34 | import type { ExtensionMetadata } from '@pixi-spine/devtools';
35 |
36 | export const stats: StatsExtension = {
37 | extension: {
38 | type: 'stats',
39 | name: 'custom-stats',
40 | },
41 | track(container: Container, state: Record) {
42 | if (container instanceof Sprite) {
43 | state.spriteCount = (state.spriteCount || 0) + 1;
44 | }
45 | },
46 | getKeys() {
47 | return ['spriteCount'];
48 | },
49 | };
50 | ```
51 |
--------------------------------------------------------------------------------
/packages/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@devtool/docs",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids",
15 | "typecheck": "tsc"
16 | },
17 | "dependencies": {
18 | "@docusaurus/core": "3.4.0",
19 | "@docusaurus/preset-classic": "3.4.0",
20 | "@mdx-js/react": "^3.0.0",
21 | "clsx": "^2.0.0",
22 | "prism-react-renderer": "^2.3.0",
23 | "react": "^18.0.0",
24 | "react-dom": "^18.0.0"
25 | },
26 | "devDependencies": {
27 | "@docusaurus/module-type-aliases": "3.4.0",
28 | "@docusaurus/tsconfig": "3.4.0",
29 | "@docusaurus/types": "3.4.0",
30 | "typescript": "~5.2.2"
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.5%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 3 chrome version",
40 | "last 3 firefox version",
41 | "last 5 safari version"
42 | ]
43 | },
44 | "engines": {
45 | "node": ">=18.0"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/packages/docs/sidebars.ts:
--------------------------------------------------------------------------------
1 | import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';
2 |
3 | /**
4 | * Creating a sidebar enables you to:
5 | - create an ordered group of docs
6 | - render a sidebar for each doc of that group
7 | - provide next/previous navigation
8 |
9 | The sidebars can be generated from the filesystem, or explicitly defined here.
10 |
11 | Create as many sidebars as you want.
12 | */
13 | const sidebars: SidebarsConfig = {
14 | // By default, Docusaurus generates a sidebar from the docs folder structure
15 | guide: [{ type: 'autogenerated', dirName: 'guide' }],
16 | plugin: [
17 | 'plugin/index',
18 | {
19 | type: 'category',
20 | label: 'Scene Extensions',
21 | items: ['plugin/properties', 'plugin/overlay', 'plugin/stats', 'plugin/tree'],
22 | collapsed: false,
23 | collapsible: false,
24 | },
25 | ],
26 |
27 | // But you can create a sidebar manually
28 | /*
29 | tutorialSidebar: [
30 | 'intro',
31 | 'hello',
32 | {
33 | type: 'category',
34 | label: 'Tutorial',
35 | items: ['tutorial-basics/create-a-document'],
36 | },
37 | ],
38 | */
39 | };
40 |
41 | export default sidebars;
42 |
--------------------------------------------------------------------------------
/packages/docs/src/css/custom.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@600&family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap');
2 | /**
3 | * Any CSS included here will be global. The classic template
4 | * bundles Infima by default. Infima is a CSS framework designed to
5 | * work well for content-centric websites.
6 | */
7 |
8 | /* You can override the default Infima variables here. */
9 | :root {
10 | --ifm-font-family-base: 'Roboto';
11 | --ifm-color-primary: #e91e63;
12 | --ifm-color-primary-dark: #d81557;
13 | --ifm-color-primary-darker: #cc1452;
14 | --ifm-color-primary-darkest: #a81044;
15 | --ifm-color-primary-light: #eb3674;
16 | --ifm-color-primary-lighter: #ed427c;
17 | --ifm-color-primary-lightest: #f06695;
18 | --ifm-code-font-size: 95%;
19 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
20 | --ifm-hr-background-color: var(--ifm-color-gray-700);
21 | }
22 |
23 | /* For readability concerns, you should choose a lighter palette in dark mode. */
24 | /* [data-theme='dark'] {
25 | --ifm-color-primary: #25c2a0;
26 | --ifm-color-primary-dark: #21af90;
27 | --ifm-color-primary-darker: #1fa588;
28 | --ifm-color-primary-darkest: #1a8870;
29 | --ifm-color-primary-light: #29d5b0;
30 | --ifm-color-primary-lighter: #32d8b4;
31 | --ifm-color-primary-lightest: #4fddbf;
32 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
33 | } */
34 |
35 | .header-twitter-link::before {
36 | width: 26px;
37 | height: 24px;
38 | background: url('/social/twitter.svg') no-repeat center;
39 | }
40 |
41 | .header-discord-link::before {
42 | width: 30px;
43 | height: 24px;
44 | background: url('/social/discord.svg') no-repeat center;
45 | }
46 |
47 | .header-github-link::before {
48 | width: 24px;
49 | height: 24px;
50 | background: url('/social/github.svg') no-repeat center;
51 | }
52 |
53 | .header-open-col-link::before {
54 | width: 24px;
55 | height: 24px;
56 | background: url('/social/open-col-icon.svg') no-repeat center;
57 | /* set icons to white when dark mode enabled and black when light */
58 | [data-theme='dark'] &::before {
59 | filter: unset;
60 | }
61 | }
62 |
63 | .header-link {
64 | &::before {
65 | content: '';
66 | display: flex;
67 | background-size: contain;
68 | }
69 |
70 | /* set icons to white when dark mode enabled and black when light */
71 | [data-theme='dark'] &::before {
72 | filter: invert(1);
73 | }
74 |
75 | &:hover {
76 | opacity: 0.6;
77 | }
78 | }
79 |
80 | /* hr {
81 |
82 | }5f5f5f */
83 |
--------------------------------------------------------------------------------
/packages/docs/src/pages/index.module.css:
--------------------------------------------------------------------------------
1 | /**
2 | * CSS files with the .module.css suffix will be treated as CSS modules
3 | * and scoped locally.
4 | */
5 |
6 | .heroBanner {
7 | padding: 4rem 0;
8 | text-align: center;
9 | position: relative;
10 | overflow: hidden;
11 | }
12 |
13 | @media screen and (max-width: 996px) {
14 | .heroBanner {
15 | padding: 2rem;
16 | }
17 | }
18 |
19 | .buttons {
20 | display: flex;
21 | align-items: center;
22 | justify-content: center;
23 | gap: 2rem;
24 | }
25 |
--------------------------------------------------------------------------------
/packages/docs/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import Link from '@docusaurus/Link';
3 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
4 | import Layout from '@theme/Layout';
5 | import Heading from '@theme/Heading';
6 |
7 | import styles from './index.module.css';
8 |
9 | function HomepageHeader() {
10 | const { siteConfig } = useDocusaurusContext();
11 | return (
12 |
34 | );
35 | }
36 |
37 | export default function Home(): JSX.Element {
38 | const { siteConfig } = useDocusaurusContext();
39 | return (
40 |
41 |
42 |
45 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/packages/docs/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/.nojekyll
--------------------------------------------------------------------------------
/packages/docs/static/gif/devtool-asset-search.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/gif/devtool-asset-search.gif
--------------------------------------------------------------------------------
/packages/docs/static/gif/devtool-asset-selection.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/gif/devtool-asset-selection.gif
--------------------------------------------------------------------------------
/packages/docs/static/gif/devtool-asset-sorting.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/gif/devtool-asset-sorting.gif
--------------------------------------------------------------------------------
/packages/docs/static/gif/devtool-copy.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/gif/devtool-copy.gif
--------------------------------------------------------------------------------
/packages/docs/static/gif/devtool-delete.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/gif/devtool-delete.gif
--------------------------------------------------------------------------------
/packages/docs/static/gif/devtool-highlight.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/gif/devtool-highlight.gif
--------------------------------------------------------------------------------
/packages/docs/static/gif/devtool-lock.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/gif/devtool-lock.gif
--------------------------------------------------------------------------------
/packages/docs/static/gif/devtool-properties.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/gif/devtool-properties.gif
--------------------------------------------------------------------------------
/packages/docs/static/gif/devtool-rename.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/gif/devtool-rename.gif
--------------------------------------------------------------------------------
/packages/docs/static/gif/devtool-reparent.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/gif/devtool-reparent.gif
--------------------------------------------------------------------------------
/packages/docs/static/gif/devtool-right-click.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/gif/devtool-right-click.gif
--------------------------------------------------------------------------------
/packages/docs/static/gif/devtool-search.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/gif/devtool-search.gif
--------------------------------------------------------------------------------
/packages/docs/static/gif/devtool-selection.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/gif/devtool-selection.gif
--------------------------------------------------------------------------------
/packages/docs/static/gif/devtool-stats.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/gif/devtool-stats.gif
--------------------------------------------------------------------------------
/packages/docs/static/img/devtool-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/img/devtool-screenshot.png
--------------------------------------------------------------------------------
/packages/docs/static/img/docusaurus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/img/docusaurus.png
--------------------------------------------------------------------------------
/packages/docs/static/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/img/favicon.png
--------------------------------------------------------------------------------
/packages/docs/static/img/ogimage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/docs/static/img/ogimage.png
--------------------------------------------------------------------------------
/packages/docs/static/social/discord.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/docs/static/social/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/docs/static/social/open-col-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
9 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/packages/docs/static/social/twitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/packages/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@docusaurus/tsconfig",
3 | "compilerOptions": {
4 | "baseUrl": "."
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pixi Example
8 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/packages/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@devtool/example",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "build": "vite build",
8 | "watch": "vite dev",
9 | "start": "npm run watch"
10 | },
11 | "dependencies": {
12 | "pixi.js": "^8.2.2",
13 | "@pixi/devtools": "*"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/example/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 |
3 | export default defineConfig({
4 | build: {
5 | sourcemap: true,
6 | },
7 | server: {
8 | port: 3000,
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/packages/frontend/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@devtool/frontend",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "scripts": {},
7 | "dependencies": {
8 | "@radix-ui/react-checkbox": "^1.1.1",
9 | "@radix-ui/react-collapsible": "^1.0.3",
10 | "@radix-ui/react-context-menu": "^2.2.1",
11 | "@radix-ui/react-dropdown-menu": "^2.0.6",
12 | "@radix-ui/react-icons": "^1.3.0",
13 | "@radix-ui/react-popover": "^1.0.7",
14 | "@radix-ui/react-scroll-area": "^1.1.0",
15 | "@radix-ui/react-select": "^2.1.1",
16 | "@radix-ui/react-separator": "^1.0.3",
17 | "@radix-ui/react-slider": "^1.2.0",
18 | "@radix-ui/react-slot": "^1.0.2",
19 | "@radix-ui/react-radio-group": "^1.2.0",
20 | "@radix-ui/react-switch": "^1.1.0",
21 | "@radix-ui/react-tabs": "^1.0.4",
22 | "@radix-ui/react-toggle": "^1.1.0",
23 | "@radix-ui/react-toggle-group": "^1.0.4",
24 | "@radix-ui/react-tooltip": "^1.1.2",
25 | "class-variance-authority": "^0.7.0",
26 | "clsx": "^2.1.1",
27 | "fuse.js": "^7.0.0",
28 | "react": "^18.3.1",
29 | "react-arborist": "^3.4.0",
30 | "react-color": "^2.19.3",
31 | "react-dom": "^18.3.1",
32 | "react-icons": "^5.2.1",
33 | "react-resizable-panels": "^2.0.20",
34 | "react-virtualized-auto-sizer": "^1.0.24",
35 | "react-syntax-highlighter": "npm:@fengkx/react-syntax-highlighter@15.6.1",
36 | "smoothie": "^1.36.1",
37 | "tailwind-merge": "^2.4.0",
38 | "tailwindcss-animate": "^1.0.7",
39 | "zustand": "^4.5.4"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/frontend/src/assets/icon-active-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/frontend/src/assets/icon-active-48.png
--------------------------------------------------------------------------------
/packages/frontend/src/assets/transparent-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/packages/frontend/src/assets/transparent.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/collapsible/collapsible-section.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '../../lib/utils';
2 | import { useState } from 'react';
3 | import { FaAngleDown, FaCopy as CopyIcon } from 'react-icons/fa6';
4 | import { useLocalStorage } from '../../lib/localStorage';
5 |
6 | export const SaveCollapsibleSection: React.FC = ({
7 | storageKey,
8 | ...props
9 | }) => {
10 | const [defaultCollapsed, setDefaultCollapsed] = useLocalStorage(storageKey, props.defaultCollapsed);
11 |
12 | return ;
13 | };
14 |
15 | interface CollapsibleSectionProps extends React.HTMLAttributes {
16 | children: React.ReactNode;
17 | title: string;
18 | onCopy?: () => void;
19 | defaultCollapsed?: boolean;
20 | onCollapse?: (collapsed: boolean) => void;
21 | }
22 | export const CollapsibleSection = ({
23 | className,
24 | children,
25 | title,
26 | onCopy,
27 | defaultCollapsed,
28 | onCollapse,
29 | }: CollapsibleSectionProps) => {
30 | const [collapsed, setCollapsed] = useState(defaultCollapsed ?? false);
31 |
32 | return (
33 | <>
34 | {
40 | setCollapsed(!collapsed);
41 | onCollapse && onCollapse(!collapsed);
42 | }}
43 | >
44 |
45 | {onCopy && (
46 |
{
48 | onCopy();
49 | e.stopPropagation();
50 | }}
51 | className="hover:fill-primary cursor-pointer opacity-45 hover:opacity-100"
52 | />
53 | )}
54 | {title}
55 |
56 |
57 |
58 | {!collapsed && children}
59 | >
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/collapsible/collapsible-split.tsx:
--------------------------------------------------------------------------------
1 | import type { PanelProps } from 'react-resizable-panels';
2 | import { PanelGroup, Panel, PanelResizeHandle } from 'react-resizable-panels';
3 | import { CollapsibleSection } from './collapsible-section';
4 |
5 | interface CollapsibleSectionProps {
6 | left: React.ReactNode;
7 | right: React.ReactNode;
8 | rightOptions?: PanelProps;
9 | leftOptions?: PanelProps;
10 | title?: string;
11 | }
12 | export const CollapsibleSplit: React.FC = ({
13 | title,
14 | left,
15 | right,
16 | rightOptions,
17 | leftOptions,
18 | }) => {
19 | return (
20 |
21 |
22 |
23 |
24 |
25 | {left}
26 |
27 |
28 |
29 | {right}
30 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | import { FaMoon, FaSun } from 'react-icons/fa6';
2 |
3 | import { Button } from './ui/button';
4 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './ui/dropdown-menu';
5 | import { useTheme } from './theme-provider';
6 |
7 | export function ModeToggle() {
8 | const { setTheme } = useTheme();
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | Toggle theme
17 |
18 |
19 |
20 | setTheme('light')}>Light
21 | setTheme('dark')}>Dark
22 | setTheme('system')}>System
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/properties/boolean-property.tsx:
--------------------------------------------------------------------------------
1 | import type { SwitchProps } from '@radix-ui/react-switch';
2 | import { Switch } from '../ui/switch';
3 | import type { PropertyPanelData } from './propertyTypes';
4 |
5 | export const BooleanProperty: React.FC = ({ value, entry }) => {
6 | return (
7 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/properties/button-property.tsx:
--------------------------------------------------------------------------------
1 | import type { ButtonProps } from '../ui/button';
2 | import { Button } from '../ui/button';
3 | import type { PropertyPanelData } from './propertyTypes';
4 |
5 | export type ButtonFnProps = { label: string } & ButtonProps;
6 | export const ButtonProperty: React.FC = ({ entry }) => {
7 | return (
8 | entry.onChange(true)}
13 | {...(entry.options as ButtonFnProps)}
14 | >
15 | {(entry.options as ButtonFnProps)?.label ?? entry.label ?? 'Click'}
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/properties/color-property.tsx:
--------------------------------------------------------------------------------
1 | import type { ColorProps } from '../ui/color';
2 | import { ColorInput } from '../ui/color';
3 | import type { PropertyPanelData } from './propertyTypes';
4 |
5 | export const ColorProperty: React.FC = ({ value, entry }) => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/properties/multi-property.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import type { PropertyOptions, PropertyPanelData, PropertyTypes } from './propertyTypes';
3 | import { propertyMap } from './propertyTypes';
4 |
5 | export interface MultiProps {
6 | inputs: {
7 | value: any;
8 | prop: string;
9 | entry: {
10 | label?: string;
11 | type: PropertyTypes;
12 | options?: PropertyOptions;
13 | };
14 | }[];
15 | }
16 |
17 | export const MultiProperty: React.FC = ({ entry }) => {
18 | const options = entry.options as MultiProps;
19 | const initializeValues = () => options.inputs.map((input) => input.value);
20 | const [currentValues, setCurrentValues] = useState(initializeValues);
21 | const inputsDependency =
22 | options.inputs.length + options.inputs.map((input) => `${input.prop}${input.value}`).join(',');
23 |
24 | useEffect(() => {
25 | setCurrentValues(initializeValues());
26 | // eslint-disable-next-line react-hooks/exhaustive-deps
27 | }, [inputsDependency]);
28 |
29 | const comps = options.inputs.map((input, i) => {
30 | const PropertyComponent = propertyMap[input.entry.type];
31 | const onChange = (value: string) => {
32 | setCurrentValues((prevValues) => {
33 | const newValues = [...prevValues];
34 | newValues[i] = JSON.parse(value);
35 | entry.onChange(JSON.stringify([input.prop, newValues]));
36 | return newValues;
37 | });
38 | };
39 | const props = { ...input, entry: { ...input.entry, onChange }, value: currentValues[i] } as PropertyPanelData;
40 | return (
41 |
42 |
43 |
{input.entry.label}
44 |
45 |
46 |
47 | );
48 | });
49 |
50 | return {comps}
;
51 | };
52 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/properties/number-property.tsx:
--------------------------------------------------------------------------------
1 | import type { InputProps } from '../ui/input';
2 | import { Input } from '../ui/input';
3 | import type { PropertyPanelData } from './propertyTypes';
4 |
5 | export const NumberProperty: React.FC = ({ value, entry }) => {
6 | return (
7 | entry.onChange(Number(e.target.value))}
12 | className="border-border hover:border-secondary focus:border-secondary h-6 w-full rounded text-xs outline-none"
13 | />
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/properties/propertyEntry.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { cn } from '../../lib/utils';
3 | import { Separator } from '../ui/separator';
4 | import { TooltipWrapper } from '../ui/tooltip';
5 |
6 | interface PropertyEntryProps {
7 | title: string;
8 | input: React.ReactNode;
9 | className?: string;
10 | tooltip?: string;
11 | isLast: boolean;
12 | }
13 | export const PropertyEntry: React.FC = memo(({ title, input, className, tooltip, isLast }) => {
14 | return (
15 | <>
16 |
17 | {tooltip ? (
18 |
19 | {title}
} tip={tooltip} />
20 |
21 | ) : (
22 | {title}
23 | )}
24 | {input}
25 |
26 | {!isLast && }
27 | >
28 | );
29 | });
30 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/properties/propertyTypes.tsx:
--------------------------------------------------------------------------------
1 | import type { SliderProps } from '@radix-ui/react-slider';
2 | import type { SwitchProps } from '@radix-ui/react-switch';
3 | import { memo } from 'react';
4 | import { isDifferent } from '../../lib/utils';
5 | import type { ColorProps } from '../ui/color';
6 | import type { InputProps } from '../ui/input';
7 | import type { Vector2Props, VectorXProps } from '../ui/vector2';
8 | import { BooleanProperty } from './boolean-property';
9 | import type { ButtonFnProps } from './button-property';
10 | import { ButtonProperty } from './button-property';
11 | import { ColorProperty } from './color-property';
12 | import { NumberProperty } from './number-property';
13 | import { RangeProperty } from './range-property';
14 | import { SelectProperty } from './select-property';
15 | import { TextProperty } from './text-property';
16 | import { Vector2Property, VectorXProperty } from './vector-property';
17 | import { MultiProperty } from './multi-property';
18 |
19 | export type PropertyTypes =
20 | | 'boolean'
21 | | 'number'
22 | | 'range'
23 | | 'select'
24 | | 'text'
25 | | 'button'
26 | | 'vector2'
27 | | 'vectorX'
28 | | 'color'
29 | | 'multi';
30 |
31 | export type SelectionProps = { options: string[] };
32 |
33 | export type PropertyOptions =
34 | | InputProps
35 | | SwitchProps
36 | | Vector2Props
37 | | SliderProps
38 | | SelectionProps
39 | | VectorXProps
40 | | ColorProps
41 | | ButtonFnProps;
42 |
43 | export type PropertyPanelData = {
44 | value: any;
45 | prop: string;
46 | entry: {
47 | section: string;
48 | tooltip?: string;
49 | label?: string;
50 | type: PropertyTypes;
51 | options?: PropertyOptions;
52 | onChange: (value: string | number | boolean) => void;
53 | };
54 | };
55 |
56 | const memoTest = (props: any, nextProps: any) => {
57 | return !isDifferent(props, nextProps);
58 | };
59 |
60 | export const propertyMap: Record> = {
61 | boolean: memo(BooleanProperty, memoTest),
62 | number: memo(NumberProperty, memoTest),
63 | vector2: memo(Vector2Property, memoTest),
64 | range: memo(RangeProperty, memoTest),
65 | select: memo(SelectProperty, memoTest),
66 | text: memo(TextProperty, memoTest),
67 | button: memo(ButtonProperty, memoTest),
68 | vectorX: memo(VectorXProperty, memoTest),
69 | color: memo(ColorProperty, memoTest),
70 | multi: memo(MultiProperty, memoTest),
71 | };
72 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/properties/range-property.tsx:
--------------------------------------------------------------------------------
1 | import type { SliderProps } from '@radix-ui/react-slider';
2 | import { Slider } from '../ui/slider';
3 | import type { PropertyPanelData } from './propertyTypes';
4 |
5 | export const RangeProperty: React.FC = ({ value, entry }) => {
6 | return (
7 | entry.onChange(JSON.stringify(value))}
11 | className="border-border hover:border-secondary focus:border-secondary h-6 w-full rounded outline-none"
12 | />
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/properties/select-property.tsx:
--------------------------------------------------------------------------------
1 | import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
2 | import type { PropertyPanelData, SelectionProps } from './propertyTypes';
3 |
4 | export const SelectProperty: React.FC = ({ value, entry }) => {
5 | return (
6 | entry.onChange(JSON.stringify(value))}>
7 |
8 |
9 |
10 |
11 |
12 | {(entry.options as SelectionProps).options.map((option: string) => (
13 |
14 | {option}
15 |
16 | ))}
17 |
18 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/properties/text-property.tsx:
--------------------------------------------------------------------------------
1 | import type { InputProps } from '../ui/input';
2 | import { Input } from '../ui/input';
3 | import type { PropertyPanelData } from './propertyTypes';
4 |
5 | export const TextProperty: React.FC = ({ value, entry }) => {
6 | return (
7 | entry.onChange(JSON.stringify(e.target.value))}
12 | className="border-border hover:border-secondary focus:border-secondary h-6 w-full rounded text-xs outline-none"
13 | />
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/properties/vector-property.tsx:
--------------------------------------------------------------------------------
1 | import type { Vector2Props, VectorXProps } from '../ui/vector2';
2 | import { Vector2, VectorX } from '../ui/vector2';
3 | import type { PropertyPanelData } from './propertyTypes';
4 |
5 | export const Vector2Property: React.FC = ({ value, entry }) => {
6 | return ;
7 | };
8 |
9 | export const VectorXProperty: React.FC = ({ value, entry }) => {
10 | return ;
11 | };
12 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useState } from 'react';
2 |
3 | type Theme = 'dark' | 'light' | 'system';
4 |
5 | type ThemeProviderProps = {
6 | children: React.ReactNode;
7 | defaultTheme?: Theme;
8 | storageKey?: string;
9 | };
10 |
11 | type ThemeProviderState = {
12 | theme: Theme;
13 | setTheme: (theme: Theme) => void;
14 | };
15 |
16 | const initialState: ThemeProviderState = {
17 | theme: 'system',
18 | setTheme: () => null,
19 | };
20 |
21 | const ThemeProviderContext = createContext(initialState);
22 |
23 | export function ThemeProvider({
24 | children,
25 | defaultTheme = 'system',
26 | storageKey = 'vite-ui-theme',
27 | ...props
28 | }: ThemeProviderProps) {
29 | const [theme, setTheme] = useState(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme);
30 |
31 | useEffect(() => {
32 | const root = window.document.documentElement;
33 |
34 | root.classList.remove('light', 'dark');
35 |
36 | if (theme === 'system') {
37 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
38 |
39 | root.classList.add(systemTheme);
40 | return;
41 | }
42 |
43 | root.classList.add(theme);
44 | }, [theme]);
45 |
46 | const value = {
47 | theme,
48 | setTheme: (theme: Theme) => {
49 | localStorage.setItem(storageKey, theme);
50 | setTheme(theme);
51 | },
52 | };
53 |
54 | return (
55 |
56 | {children}
57 |
58 | );
59 | }
60 |
61 | export const useTheme = () => {
62 | const context = useContext(ThemeProviderContext);
63 |
64 | if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider');
65 |
66 | return context;
67 | };
68 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import { cva } from 'class-variance-authority';
4 | import type { VariantProps } from 'class-variance-authority';
5 |
6 | import { cn } from '../../lib/utils';
7 |
8 | const buttonVariants = cva(
9 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
10 | {
11 | variants: {
12 | variant: {
13 | default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
14 | destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
15 | outline: 'border border-primary bg-background shadow-sm hover:bg-primary/90 hover:text-accent-foreground',
16 | secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
17 | ghost: 'hover:bg-accent hover:text-accent-foreground',
18 | link: 'text-primary underline-offset-4 hover:underline',
19 | },
20 | size: {
21 | default: 'h-9 px-4 py-2',
22 | sm: 'h-8 rounded-md px-3 text-xs',
23 | xs: 'h-4 rounded-md px-2 text-xs',
24 | lg: 'h-10 rounded-md px-8',
25 | icon: 'h-9 w-9',
26 | },
27 | },
28 | defaultVariants: {
29 | variant: 'default',
30 | size: 'default',
31 | },
32 | },
33 | );
34 |
35 | export interface ButtonProps
36 | extends React.ButtonHTMLAttributes,
37 | VariantProps {
38 | asChild?: boolean;
39 | }
40 |
41 | const Button = React.forwardRef(
42 | ({ className, variant, size, asChild = false, ...props }, ref) => {
43 | const Comp = asChild ? Slot : 'button';
44 | return ;
45 | },
46 | );
47 | Button.displayName = 'Button';
48 |
49 | export { Button };
50 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
3 | import { CheckIcon } from '@radix-ui/react-icons';
4 |
5 | import { cn } from '../../lib/utils';
6 |
7 | const Checkbox = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
19 |
20 |
21 |
22 |
23 | ));
24 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
25 |
26 | export { Checkbox };
27 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/clipboard.tsx:
--------------------------------------------------------------------------------
1 | import { FaRegCopy as ClipboardIcon, FaCheck as CheckIcon } from 'react-icons/fa6';
2 | import { useState, useEffect } from 'react';
3 | import { Button } from './button';
4 | import { copyToClipboard } from '@devtool/frontend/lib/utils';
5 |
6 | export const CopyToClipboardButton: React.FC<{ data: string }> = ({ data }) => {
7 | const [copied, setCopied] = useState(false);
8 |
9 | const onClick = (data: string) => {
10 | copyToClipboard(data);
11 | setCopied(true);
12 | };
13 |
14 | useEffect(() => {
15 | if (copied) {
16 | const timer = setTimeout(() => {
17 | setCopied(false);
18 | }, 2000); // Change back to ClipboardIcon after 2 seconds
19 |
20 | return () => clearTimeout(timer); // Clean up on unmount
21 | }
22 | }, [copied]);
23 |
24 | return (
25 | onClick(data)}
30 | >
31 | {copied ? : }
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/codearea.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
3 | import { dracula } from 'react-syntax-highlighter/dist/esm/styles/prism';
4 |
5 | export const CodeArea: React.FC<{ codeString: string }> = ({ codeString }) => {
6 | return (
7 |
8 |
9 | {codeString}
10 |
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/color.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import type { ChromePickerProps } from 'react-color';
3 | import { ChromePicker } from 'react-color';
4 | import { Button } from './button';
5 |
6 | export interface ColorProps extends Omit {
7 | value: number;
8 | onChange: (color: string) => void;
9 | }
10 |
11 | export const ColorInput: React.FC = ({ value, onChange, ...rest }) => {
12 | const [displayColorPicker, setDisplayColorPicker] = useState(false);
13 |
14 | const handleClick = () => {
15 | setDisplayColorPicker(!displayColorPicker);
16 | };
17 |
18 | const handleClose = () => {
19 | setDisplayColorPicker(false);
20 | };
21 |
22 | // convert number ot hex
23 | const hexString = value?.toString(16) ?? 'ffffff';
24 |
25 | const hex = `#${'000000'.substring(0, 6 - hexString.length) + hexString}`;
26 |
27 | return (
28 |
29 | {displayColorPicker ? (
30 |
31 |
37 | {hex}
38 |
39 |
40 | onChange(JSON.stringify(updatedColor.hex))}
43 | className="relative top-2"
44 | {...rest}
45 | />
46 |
47 |
48 | ) : (
49 |
55 | {hex}
56 |
57 | //
{hex}
58 | )}
59 |
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '../../lib/utils';
4 |
5 | export interface InputProps extends React.InputHTMLAttributes {}
6 |
7 | const Input = React.forwardRef(({ className, type, ...props }, ref) => {
8 | if (!props.defaultValue) {
9 | props.value = props.value ?? '';
10 | }
11 | return (
12 |
21 | );
22 | });
23 | Input.displayName = 'Input';
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as PopoverPrimitive from '@radix-ui/react-popover';
3 |
4 | import { cn } from '../../lib/utils';
5 |
6 | const Popover = PopoverPrimitive.Root;
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger;
9 |
10 | const PopoverAnchor = PopoverPrimitive.Anchor;
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 |
31 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
32 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | // import { CheckIcon } from '@radix-ui/react-icons';
3 | import { FaCircle as CheckIcon } from 'react-icons/fa6';
4 |
5 | import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
6 |
7 | import { cn } from '../../lib/utils';
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return ;
14 | });
15 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
16 |
17 | const RadioGroupItem = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => {
21 | return (
22 |
30 |
31 |
32 |
33 |
34 | );
35 | });
36 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
37 |
38 | export { RadioGroup, RadioGroupItem };
39 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as SeparatorPrimitive from '@radix-ui/react-separator';
3 |
4 | import { cn } from '../../lib/utils';
5 |
6 | const Separator = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
10 |
17 | ));
18 | Separator.displayName = SeparatorPrimitive.Root.displayName;
19 |
20 | export { Separator };
21 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as SliderPrimitive from '@radix-ui/react-slider';
3 |
4 | import { cn } from '../../lib/utils';
5 |
6 | const Slider = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
15 |
16 |
17 |
18 |
19 |
20 | ));
21 | Slider.displayName = SliderPrimitive.Root.displayName;
22 |
23 | export { Slider };
24 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/svg-texture.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useTheme } from '../theme-provider';
3 |
4 | interface SVGTextureProps {
5 | color: string;
6 | }
7 |
8 | export const SVGTexture: React.FC = ({ color }) => {
9 | const { theme } = useTheme();
10 |
11 | const color1 = theme === 'dark' ? 'hsla(0,0%,15%,0.75)' : 'hsla(0,0%,94%,0.75)';
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as SwitchPrimitives from '@radix-ui/react-switch';
3 |
4 | import { cn } from '../../lib/utils';
5 |
6 | const Switch = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
23 |
24 | ));
25 | Switch.displayName = SwitchPrimitives.Root.displayName;
26 |
27 | export { Switch };
28 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '../../lib/utils';
4 |
5 | export interface TextareaProps extends React.TextareaHTMLAttributes {}
6 |
7 | const Textarea = React.forwardRef(({ className, ...props }, ref) => {
8 | return (
9 |
17 | );
18 | });
19 | Textarea.displayName = 'Textarea';
20 |
21 | export { Textarea };
22 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
3 | import type { VariantProps } from 'class-variance-authority';
4 |
5 | import { cn } from '../../lib/utils';
6 | import { toggleVariants } from '../../components/ui/toggle';
7 |
8 | const ToggleGroupContext = React.createContext>({
9 | size: 'default',
10 | variant: 'default',
11 | });
12 |
13 | const ToggleGroup = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef & VariantProps
16 | >(({ className, variant, size, children, ...props }, ref) => (
17 |
18 | {children}
19 |
20 | ));
21 |
22 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
23 |
24 | const ToggleGroupItem = React.forwardRef<
25 | React.ElementRef,
26 | React.ComponentPropsWithoutRef & VariantProps
27 | >(({ className, children, variant, size, ...props }, ref) => {
28 | const context = React.useContext(ToggleGroupContext);
29 |
30 | return (
31 |
42 | {children}
43 |
44 | );
45 | });
46 |
47 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
48 |
49 | export { ToggleGroup, ToggleGroupItem };
50 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as TogglePrimitive from '@radix-ui/react-toggle';
3 | import { cva } from 'class-variance-authority';
4 | import type { VariantProps } from 'class-variance-authority';
5 |
6 | import { cn } from '../../lib/utils';
7 |
8 | const toggleVariants = cva(
9 | 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
10 | {
11 | variants: {
12 | variant: {
13 | default: 'bg-transparent',
14 | outline:
15 | 'border border-primary bg-background shadow-sm hover:bg-primary/90 hover:text-accent-foreground data-[state=on]:bg-primary',
16 | ghost: 'hover:bg-accent hover:text-accent-foreground',
17 | },
18 | size: {
19 | default: 'h-9 px-3',
20 | sm: 'h-8 px-2',
21 | xs: 'h-4 px-2 text-xs',
22 | lg: 'h-10 px-3',
23 | icon: 'h-9 w-9 data-[state=on]:border-b-2 data-[state=on]:border-primary',
24 | },
25 | },
26 | defaultVariants: {
27 | variant: 'default',
28 | size: 'default',
29 | },
30 | },
31 | );
32 |
33 | const Toggle = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef & VariantProps
36 | >(({ className, variant, size, ...props }, ref) => (
37 |
38 | ));
39 |
40 | Toggle.displayName = TogglePrimitive.Root.displayName;
41 |
42 | export { Toggle, toggleVariants };
43 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as TooltipPrimitive from '@radix-ui/react-tooltip';
3 | import { CodeArea } from './codearea';
4 |
5 | import { cn } from '../../lib/utils';
6 |
7 | const TooltipProvider = TooltipPrimitive.Provider;
8 |
9 | const Tooltip = TooltipPrimitive.Root;
10 |
11 | const TooltipTrigger = TooltipPrimitive.Trigger;
12 |
13 | const TooltipContent = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef
16 | >(({ className, sideOffset = 4, ...props }, ref) => (
17 |
26 | ));
27 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
28 |
29 | const TooltipWrapper: React.FC<{
30 | trigger: React.ReactNode;
31 | tip: string;
32 | triggerProps?: React.ComponentPropsWithoutRef;
33 | contentProps?: React.ComponentPropsWithoutRef;
34 | providerProps?: Omit, 'children'>;
35 | }> = ({ trigger, tip, triggerProps, contentProps, providerProps }) => {
36 | const parts = tip.split(/{{(.*?)}}/g);
37 |
38 | const content = parts.map((part, i) => {
39 | const isCode = i % 2 === 1; // code parts are at odd indices after split
40 | return isCode ? (
41 |
42 | ) : (
43 |
44 | {part}
45 |
46 | );
47 | });
48 |
49 | return (
50 |
51 |
52 | {trigger}
53 |
54 |
55 | {content}
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, TooltipWrapper };
64 |
--------------------------------------------------------------------------------
/packages/frontend/src/components/ui/vector2.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '../../lib/utils';
2 | import type { InputProps } from './input';
3 | import { Input } from './input';
4 |
5 | export interface Vector2Props {
6 | x: InputProps & { label: string };
7 | y: InputProps & { label: string };
8 | className?: string;
9 | onChange: (val: string) => void;
10 | value: [number, number] | null;
11 | }
12 | export const Vector2: React.FC = ({ x, y, className, onChange, value }) => {
13 | value = value || ([] as unknown as [number, number]);
14 | x.onChange = (event) => {
15 | const val = Number(event.target.value);
16 | onChange(JSON.stringify([val, value[1]]));
17 | };
18 | y.onChange = (event) => {
19 | const val = Number(event.target.value);
20 | onChange(JSON.stringify([value[0], val]));
21 | };
22 |
23 | x.value = value[0];
24 | y.value = value[1];
25 |
26 | return (
27 |
45 | );
46 | };
47 |
48 | export interface VectorXProps {
49 | inputs: (InputProps & { label: string })[];
50 | onChange: (value: string) => void;
51 | value: number[] | null;
52 | }
53 |
54 | export const VectorX: React.FC = ({ inputs, onChange, value }) => {
55 | value = value || [];
56 | inputs.forEach((vector, index) => {
57 | vector.onChange = (e) => {
58 | const newValue = [...value];
59 | newValue[index] = Number(e.target.value);
60 | onChange(JSON.stringify(newValue));
61 | };
62 |
63 | vector.value = value[index];
64 | });
65 |
66 | return (
67 |
68 | {inputs.map((input, index) => (
69 |
70 |
{input.label}
71 |
76 |
77 | ))}
78 |
79 | );
80 | };
81 |
--------------------------------------------------------------------------------
/packages/frontend/src/lib/interval.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | export const useInterval = (callback: () => void, delay: number | null) => {
4 | const savedCallback = useRef<() => void>(() => {});
5 |
6 | // Remember the latest callback.
7 | useEffect(() => {
8 | savedCallback.current = callback;
9 | }, [callback]);
10 |
11 | // Set up the interval.
12 | useEffect(() => {
13 | function tick() {
14 | savedCallback.current();
15 | }
16 |
17 | // Execute the callback immediately before setting the interval
18 | if (delay !== null) {
19 | tick(); // Execute callback immediately
20 | const id = setInterval(tick, delay);
21 | return () => clearInterval(id);
22 | }
23 | }, [delay]); // Execute this effect when `delay` changes
24 | };
25 |
--------------------------------------------------------------------------------
/packages/frontend/src/lib/localStorage.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | function getStorageValue(key: string, defaultValue: T) {
4 | // getting stored value
5 | const saved = localStorage.getItem(key);
6 | const initial = JSON.parse(saved as string) as T;
7 | return initial || defaultValue;
8 | }
9 |
10 | export const useLocalStorage = (key: string, defaultValue: T) => {
11 | const [value, setValue] = useState(() => {
12 | return getStorageValue(key, defaultValue);
13 | });
14 |
15 | useEffect(() => {
16 | // storing input name
17 | localStorage.setItem(key, JSON.stringify(value));
18 | }, [key, value]);
19 |
20 | return [value, setValue] as const;
21 | };
22 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/assets/AssetsPanel.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useDevtoolStore } from '../../App';
3 | import { CollapsibleSplit } from '../../components/collapsible/collapsible-split';
4 | import { GPUTextures } from './gpuTextures/GPUTextures';
5 | import { TextureProperties } from './gpuTextures/TextureProperties';
6 | import { TextureStats } from './gpuTextures/TextureStats';
7 |
8 | export const AssetsPanel = () => {
9 | const [version, setVersion] = useState(null);
10 | const bridge = useDevtoolStore.use.bridge();
11 |
12 | useEffect(() => {
13 | async function fetchData() {
14 | const res = await bridge!('window.__PIXI_DEVTOOLS_WRAPPER__.majorVersion');
15 | setVersion(res as string);
16 | }
17 | fetchData();
18 | }, [bridge]);
19 |
20 | if (!version || Number(version) < 8) {
21 | return (
22 |
23 |
24 | This panel is only available for PixiJS 8 and above
25 |
26 |
27 | );
28 | }
29 |
30 | return (
31 |
32 |
33 | }
35 | right={ }
36 | rightOptions={{ maxSize: 35 }}
37 | title="Textures"
38 | />
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/assets/assets.ts:
--------------------------------------------------------------------------------
1 | import type { ZustSet } from '../../lib/utils';
2 | import type { ALPHA_MODES, TEXTURE_DIMENSIONS, TEXTURE_FORMATS } from 'pixi.js';
3 | import type { RemoveSetters } from '../../types';
4 |
5 | export interface TextureDataState {
6 | gpuSize: number;
7 | pixelWidth: number;
8 | pixelHeight: number;
9 | width: number;
10 | height: number;
11 | mipLevelCount: number;
12 | autoGenerateMipmaps: boolean;
13 | format: TEXTURE_FORMATS;
14 | dimension: TEXTURE_DIMENSIONS;
15 | alphaMode: ALPHA_MODES;
16 | antialias: boolean;
17 | destroyed: boolean;
18 | isPowerOfTwo: boolean;
19 | autoGarbageCollect: boolean;
20 | blob: string | null;
21 | isLoaded: boolean;
22 | name: string;
23 | }
24 |
25 | export interface TextureState {
26 | selectedTexture: TextureDataState | null;
27 | setSelectedTexture: (texture: TextureDataState | null) => void;
28 |
29 | textures: TextureDataState[];
30 | setTextures: (textures: TextureDataState[]) => void;
31 | }
32 |
33 | export const textureStateSlice = (set: ZustSet) => ({
34 | setSelectedTexture: (texture: TextureDataState | null) => set((state) => ({ ...state, selectedTexture: texture })),
35 | setTextures: (textures: TextureDataState[]) => set((state) => ({ ...state, textures })),
36 | });
37 |
38 | export const textureStateSelectors: RemoveSetters = {
39 | selectedTexture: null,
40 | textures: [],
41 | };
42 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/assets/gpuTextures/TextureProperties.tsx:
--------------------------------------------------------------------------------
1 | import { PropertyEntry } from '../../../components/properties/propertyEntry';
2 | import { useDevtoolStore } from '../../../App';
3 | import type { PropertyPanelData } from '../../../components/properties/propertyTypes';
4 | import { propertyMap } from '../../../components/properties/propertyTypes';
5 | import { formatCamelCase } from '../../../lib/utils';
6 |
7 | interface PanelProps {
8 | children: React.ReactNode;
9 | }
10 | const Panel: React.FC = ({ children }) => {
11 | return (
12 | <>
13 |
21 | >
22 | );
23 | };
24 |
25 | export const TextureProperties = () => {
26 | const selectedTexture = useDevtoolStore.use.selectedTexture();
27 |
28 | if (!selectedTexture) {
29 | return (
30 |
31 | Select a texture to view its properties
32 |
33 | );
34 | }
35 | const TextComponent = propertyMap.text;
36 | const textOptions = { options: { disabled: true } };
37 | const name = selectedTexture.name === '' ? 'Unnamed' : selectedTexture.name.split('/').pop();
38 |
39 | return (
40 |
41 |
42 |
43 |
44 |
45 | {Object.keys(selectedTexture).map((key, i) => {
46 | if (key === 'isLoaded' || key === 'blob') {
47 | return null;
48 | }
49 | let value = selectedTexture[key as keyof typeof selectedTexture];
50 |
51 | if (key === 'name') {
52 | value = name || 'Unnamed';
53 | } else if (key === 'gpuSize') {
54 | value = ((value as number) / (1024 * 1024)).toFixed(3) + ' MB';
55 | }
56 | return (
57 |
63 | }
64 | isLast={i === Object.keys(selectedTexture).length - 1}
65 | />
66 | );
67 | })}
68 |
69 |
70 | );
71 | };
72 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/assets/gpuTextures/TextureStats.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useDevtoolStore } from '../../../App';
3 | import { CollapsibleSection } from '../../../components/collapsible/collapsible-section';
4 | import { CanvasStatComponent } from '../../../components/smooth-charts/stat';
5 | import { useTheme } from '../../../components/theme-provider';
6 |
7 | export const TextureStats: React.FC = () => {
8 | const { theme } = useTheme();
9 | const [stats, setStats] = useState>({
10 | 'GPU Memory (MB)': 0,
11 | 'Total Textures': 0,
12 | 'Textures on GPU': 0,
13 | });
14 | const textures = useDevtoolStore.use.textures();
15 |
16 | useEffect(() => {
17 | const totalMemory = textures.reduce((acc, texture) => acc + texture.gpuSize, 0);
18 | const totalTextures = textures.length;
19 | const totalLoadedTextures = textures.filter((texture) => texture.isLoaded).length;
20 |
21 | setStats({
22 | 'GPU Memory (MB)': Number((totalMemory / 1024 / 1024).toFixed(2)),
23 | 'Total Textures': totalTextures,
24 | 'Textures on GPU': totalLoadedTextures,
25 | });
26 | }, [textures]);
27 |
28 | return (
29 |
30 |
31 |
32 | {stats &&
33 | Object.keys(stats!).map((key) => {
34 | const item = stats![key as keyof typeof stats];
35 | return (
36 |
37 |
45 |
46 | );
47 | })}
48 |
49 |
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/RenderingPanel.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useDevtoolStore } from '../../App';
3 | import { InstructionsPanel } from './InstructionsPanel';
4 | import { RenderingStats } from './RenderingStats';
5 | import { CanvasPanel } from './CanvasPanel';
6 |
7 | export const RenderingPanel = () => {
8 | const [version, setVersion] = useState(null);
9 | const bridge = useDevtoolStore.use.bridge();
10 |
11 | useEffect(() => {
12 | async function fetchData() {
13 | const res = await bridge!('window.__PIXI_DEVTOOLS_WRAPPER__.majorVersion');
14 | setVersion(res as string);
15 | }
16 | fetchData();
17 | }, [bridge]);
18 |
19 | if (!version || Number(version) < 8) {
20 | return (
21 |
22 |
23 | This panel is only available for PixiJS 8 and above
24 |
25 |
26 | );
27 | }
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/RenderingStats.tsx:
--------------------------------------------------------------------------------
1 | import { useDevtoolStore } from '../../App';
2 | import { SaveCollapsibleSection } from '../../components/collapsible/collapsible-section';
3 | import { CanvasStatComponent } from '../../components/smooth-charts/stat';
4 | import { useInterval } from '../../lib/interval';
5 | import { formatCamelCase, isDifferent } from '../../lib/utils';
6 | import type { RenderingState } from './rendering';
7 | import { useTheme } from '../../components/theme-provider';
8 |
9 | export const RenderingStats: React.FC = () => {
10 | const { theme } = useTheme();
11 | const bridge = useDevtoolStore.use.bridge();
12 | const renderingData = useDevtoolStore.use.renderingData();
13 | const setRenderingData = useDevtoolStore.use.setRenderingData();
14 |
15 | useInterval(async () => {
16 | const res = await bridge!('window.__PIXI_DEVTOOLS_WRAPPER__.rendering.captureRenderingData()');
17 |
18 | if (isDifferent(res, renderingData)) {
19 | setRenderingData(res as RenderingState['renderingData']);
20 | }
21 | }, 100);
22 |
23 | return (
24 |
25 |
26 |
27 | {renderingData &&
28 | Object.keys(renderingData!).map((key) => {
29 | const item = renderingData![key as keyof typeof renderingData];
30 | return (
31 |
32 |
40 |
41 | );
42 | })}
43 |
44 |
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/instructions/Batch.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react';
2 | import { memo } from 'react';
3 | import { propertyMap } from '../../../components/properties/propertyTypes';
4 | import { cn } from '../../../lib/utils';
5 | import { TextureViewer } from '../../assets/gpuTextures/TextureViewer';
6 | import type { BatchInstruction } from './Instructions';
7 | import { InstructionSection } from './shared/InstructionSection';
8 | import { PropertyEntries } from './shared/PropertyDisplay';
9 |
10 | export const BatchView: React.FC = memo(
11 | ({ textures, blendMode, start, size, drawTextures, action, type }) => {
12 | return (
13 |
14 |
15 |
16 |
17 | {textures.length > 0 && (
18 |
19 |
In Batch
20 |
21 | {textures.map((t, i) => (
22 |
32 | ))}
33 |
34 |
35 | )}
36 |
37 |
38 |
39 | );
40 | },
41 | );
42 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/instructions/CustomRender.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react';
2 | import { memo } from 'react';
3 | import { CollapsibleSection } from '../../../components/collapsible/collapsible-section';
4 | import { propertyMap } from '../../../components/properties/propertyTypes';
5 | import { TextureViewer } from '../../assets/gpuTextures/TextureViewer';
6 | import type { CustomRenderableInstruction } from './Instructions';
7 | import { InstructionSection } from './shared/InstructionSection';
8 | import { PropertyEntries } from './shared/PropertyDisplay';
9 |
10 | export const CustomRenderView: React.FC = memo(
11 | ({ renderable, drawTextures, type, action }) => {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | {renderable.texture && (
19 | // center the texture viewer
20 |
21 |
30 |
31 | )}
32 |
33 |
34 |
35 |
36 |
37 | );
38 | },
39 | );
40 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/instructions/Filter.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react';
2 | import { memo } from 'react';
3 | import { CollapsibleSection } from '../../../components/collapsible/collapsible-section';
4 | import { propertyMap } from '../../../components/properties/propertyTypes';
5 | import { TextureViewer } from '../../assets/gpuTextures/TextureViewer';
6 | import type { FilterInstruction } from './Instructions';
7 | import { InstructionSection } from './shared/InstructionSection';
8 | import { PropertyEntries } from './shared/PropertyDisplay';
9 | import { Shader } from './shared/Shader';
10 |
11 | export const FilterView: React.FC = memo(({ renderables, filter, drawTextures, type, action }) => {
12 | return (
13 |
14 |
15 |
16 | {filter?.map((filter, i) => (
17 |
18 |
23 |
24 | ))}
25 | {renderables.map((renderable, i) => (
26 |
27 |
28 | {renderable.texture && (
29 | // center the texture viewer
30 |
31 |
40 |
41 | )}
42 |
43 |
44 |
45 | ))}
46 |
47 |
48 | );
49 | });
50 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/instructions/Graphics.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react';
2 | import { memo } from 'react';
3 | import { CollapsibleSection } from '../../../components/collapsible/collapsible-section';
4 | import { propertyMap } from '../../../components/properties/propertyTypes';
5 | import type { GraphicsInstruction } from './Instructions';
6 | import { InstructionSection } from './shared/InstructionSection';
7 | import { PropertyEntries } from './shared/PropertyDisplay';
8 |
9 | export const GraphicsView: React.FC = memo(({ renderable, drawTextures, action, type }) => {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | {/* {renderable.texture && (
17 | // center the texture viewer
18 |
19 |
28 |
29 | )} */}
30 |
31 |
32 |
33 |
34 |
35 | );
36 | });
37 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/instructions/NineSliceSprite.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react';
2 | import { memo } from 'react';
3 | import { CollapsibleSection } from '../../../components/collapsible/collapsible-section';
4 | import { propertyMap } from '../../../components/properties/propertyTypes';
5 | import { TextureViewer } from '../../assets/gpuTextures/TextureViewer';
6 | import type { NineSliceInstruction } from './Instructions';
7 | import { InstructionSection } from './shared/InstructionSection';
8 | import { PropertyEntries } from './shared/PropertyDisplay';
9 |
10 | export const NineSliceSpriteView: React.FC = memo(
11 | ({ renderable, drawTextures, type, action }) => {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | {renderable.texture && (
19 | // center the texture viewer
20 |
21 |
30 |
31 | )}
32 |
33 |
34 |
35 |
36 |
37 | );
38 | },
39 | );
40 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/instructions/RenderGroup.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react';
2 | import { memo } from 'react';
3 | import type { BaseInstruction } from './Instructions';
4 | import { InstructionSection } from './shared/InstructionSection';
5 |
6 | export const RenderGroupView: React.FC = memo(({ drawTextures, type, action }) => {
7 | return (
8 |
9 |
10 |
11 | Render groups are used to group instructions together. They can be used to optimise your scene by reducing the
12 | amount of instruction rebuilds that need to be done.
13 |
14 |
15 | You can learn more about render groups{' '}
16 |
21 | here
22 |
23 |
24 |
25 |
26 |
27 | );
28 | });
29 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/instructions/TilingSprite.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react';
2 | import { memo } from 'react';
3 | import { CollapsibleSection } from '../../../components/collapsible/collapsible-section';
4 | import { propertyMap } from '../../../components/properties/propertyTypes';
5 | import { TextureViewer } from '../../assets/gpuTextures/TextureViewer';
6 | import type { TilingSpriteInstruction } from './Instructions';
7 | import { InstructionSection } from './shared/InstructionSection';
8 | import { PropertyEntries } from './shared/PropertyDisplay';
9 |
10 | export const TilingSpriteView: React.FC = memo(
11 | ({ renderable, drawTextures, type, action }) => {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | {renderable.texture && (
19 | // center the texture viewer
20 |
21 |
30 |
31 | )}
32 |
33 |
34 |
35 |
36 |
37 | );
38 | },
39 | );
40 |
41 | {
42 | /* }
47 | isLast={i === Object.keys(selectedTexture).length - 1}
48 | />; */
49 | }
50 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/instructions/shared/InstructionBubble.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react';
2 | import { memo } from 'react';
3 | import { FaCircle as CircleIcon } from 'react-icons/fa';
4 | import { cn } from '../../../../lib/utils';
5 | import { Texture } from './Texture';
6 |
7 | // - instruction pill rules
8 | // - circle dot color that matches the render group its a part of
9 | // - red outline if its a draw call - show blue if renderBatch
10 | // - secondary outline if hovered
11 | // - primary outline if selected
12 | // - show type, action, draw texture (if exists)
13 | export interface InstructionPillProps {
14 | onClick: () => void;
15 | selected: boolean;
16 | renderGroupColor: string;
17 | drawTextures?: string[];
18 | type: string;
19 | action: string;
20 | isDrawCall?: boolean;
21 | }
22 | export const InstructionPill: React.FC = memo(
23 | ({ onClick, selected, renderGroupColor, drawTextures, type, action, isDrawCall }) => {
24 | const border = selected ? 'bg-primary' : isDrawCall ? 'border-primary' : '';
25 | const text = selected ? 'text-white' : '';
26 | return (
27 |
35 |
36 |
37 |
38 |
39 |
Type: {type}
40 |
Action: {action}
41 | {drawTextures && drawTextures.map((texture, i) =>
)}
42 |
43 |
44 | );
45 | },
46 | );
47 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/instructions/shared/InstructionSection.tsx:
--------------------------------------------------------------------------------
1 | import { CollapsibleSection } from '../../../../components/collapsible/collapsible-section';
2 | import { propertyMap } from '../../../../components/properties/propertyTypes';
3 | import { memo } from 'react';
4 | import { PropertyEntries } from './PropertyDisplay';
5 | import { Texture } from './Texture';
6 | import { cn } from '../../../../lib/utils';
7 |
8 | interface InstructionSectionProps {
9 | drawTextures: string[];
10 | type: string;
11 | action: string;
12 | children?: React.ReactNode;
13 | }
14 | export const InstructionSection: React.FC = memo(
15 | ({ drawTextures, type, action, children }) => {
16 | return (
17 |
18 |
19 |
20 | {children}
21 | {drawTextures.length > 0 && (
22 |
23 |
Draw Call Textures
24 |
25 | {drawTextures.map((texture, i) => (
26 |
27 | ))}
28 |
29 |
30 | )}
31 |
32 |
33 | );
34 | },
35 | );
36 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/instructions/shared/PropertyDisplay.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react';
2 | import { PropertyEntry } from '../../../../components/properties/propertyEntry';
3 |
4 | interface PropertyEntriesProps {
5 | renderable: Record;
6 | propertyMap: {
7 | vector2: React.ComponentType;
8 | vectorX: React.ComponentType;
9 | text: React.ComponentType;
10 | };
11 | ignore?: string[];
12 | }
13 |
14 | export const PropertyEntries: React.FC = ({ renderable, propertyMap, ignore }) => {
15 | const formatCamelCase = (str: string) => {
16 | // Assuming formatCamelCase is defined elsewhere
17 | return str.replace(/([A-Z])/g, ' $1').replace(/^./, (str) => str.toUpperCase());
18 | };
19 |
20 | const comps = Object.entries(renderable).map(([key, value]) => {
21 | const val = value;
22 | const formattedKey = formatCamelCase(key);
23 | if (key === 'texture' || value === null || (ignore && ignore.includes(key))) {
24 | return null;
25 | }
26 |
27 | if (value instanceof Object && 'x' in value && 'y' in value && !('width' in value)) {
28 | const Vector2 = propertyMap.vector2;
29 |
30 | const props = {
31 | value: [value.x, value.y],
32 | prop: key,
33 | entry: {
34 | options: { x: { label: 'x', disabled: true }, y: { label: 'y', disabled: true } },
35 | onChange: () => {},
36 | },
37 | };
38 |
39 | return } isLast={true} />;
40 | } else if (value instanceof Object && 'x' in value && 'y' in value && 'width' in value) {
41 | const VectorX = propertyMap.vectorX;
42 |
43 | const props = {
44 | value: [value.x, value.y, value.width, value.height],
45 | prop: key,
46 | entry: {
47 | options: {
48 | inputs: [
49 | { label: 'x', disabled: true },
50 | { label: 'y', disabled: true },
51 | { label: 'width', disabled: true },
52 | { label: 'height', disabled: true },
53 | ],
54 | },
55 | onChange: () => {},
56 | },
57 | };
58 |
59 | return } isLast={true} />;
60 | }
61 |
62 | const TextComp = propertyMap.text;
63 | const props = {
64 | value: val,
65 | prop: key,
66 | entry: {
67 | options: { disabled: true },
68 | onChange: () => {},
69 | },
70 | };
71 | return } isLast={true} />;
72 | });
73 |
74 | return <>{comps}>;
75 | };
76 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/instructions/shared/Shader.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from '../../../../components/theme-provider';
2 | import React, { memo } from 'react';
3 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
4 | import { dracula, prism } from 'react-syntax-highlighter/dist/esm/styles/prism';
5 |
6 | interface ShaderProps {
7 | vertex: string;
8 | fragment: string;
9 | }
10 | export const Shader: React.FC = memo(({ vertex, fragment }) => {
11 | return (
12 | <>
13 |
14 |
15 | >
16 | );
17 | });
18 |
19 | export const ShaderView: React.FC<{ value: string; title: string }> = memo(({ value, title }) => {
20 | const valueFix = formatShader(value);
21 | const { theme } = useTheme();
22 | const style = theme === 'dark' ? dracula : prism;
23 | return (
24 |
25 |
{title}
26 |
27 |
28 | {valueFix}
29 |
30 |
31 |
32 | );
33 | });
34 |
35 | /**
36 | * formats a shader so its more pleasant to read!
37 | * @param shader - a glsl shader program source
38 | */
39 | function formatShader(shader: string): string {
40 | const spl = shader
41 | .split(/([\n{}])/g)
42 | .map((a) => a.trim())
43 | .filter((a) => a.length);
44 |
45 | let indent = '';
46 |
47 | const formatted = spl
48 | .map((a) => {
49 | let indentedLine = indent + a;
50 |
51 | if (a === '{') {
52 | indent += ' ';
53 | } else if (a === '}') {
54 | indent = indent.substr(0, indent.length - 4);
55 |
56 | indentedLine = indent + a;
57 | }
58 |
59 | return indentedLine;
60 | })
61 | .join('\n');
62 |
63 | return formatted;
64 | }
65 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/instructions/shared/State.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { StateData } from '../Instructions';
3 |
4 | export const State: React.FC = ({
5 | blend,
6 | blendMode,
7 | clockwiseFrontFace,
8 | cullMode,
9 | culling,
10 | depthMask,
11 | depthTest,
12 | offsets,
13 | polygonOffset,
14 | }) => {
15 | return (
16 |
17 |
State
18 |
State Blend: {blend ? 'true' : 'false'}
19 |
State BlendMode: {blendMode}
20 |
State Clockwise Front Face: {clockwiseFrontFace ? 'true' : 'false'}
21 |
State CullMode: {cullMode}
22 |
State Culling: {culling ? 'true' : 'false'}
23 |
State Depth Mask: {depthMask ? 'true' : 'false'}
24 |
State Depth Test: {depthTest ? 'true' : 'false'}
25 |
State Offsets: {offsets ? 'true' : 'false'}
26 |
State Polygon Offset: {polygonOffset}
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/instructions/shared/Texture.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { useTheme } from '../../../../components/theme-provider';
3 | import { cn } from '../../../../lib/utils';
4 |
5 | interface TextureProps {
6 | texture: string;
7 | size?: number;
8 | border?: string;
9 | }
10 | export const Texture: React.FC = memo(({ texture, size, border }) => {
11 | const { theme } = useTheme();
12 | const w = `w-${size ?? 40}`;
13 | const h = `h-${size ?? 40}`;
14 | const b = border ?? 'border-background';
15 | const darkColor1 = 'hsla(0, 0%, 15%, 1)';
16 | const darkColor2 = 'hsla(0, 0%, 24%, 1)';
17 | const lightColor1 = 'hsla(0, 0%, 100%, 1)';
18 | const lightColor2 = 'hsla(0, 0%, 90%, 1)';
19 | const gridColor1 = theme === 'dark' ? darkColor1 : lightColor1;
20 | const gridColor2 = theme === 'dark' ? darkColor2 : lightColor2;
21 | const svgString = encodeURIComponent(
22 | `
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | `,
33 | );
34 | return (
35 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | });
52 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/rendering/instructions/test.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixijs/devtools/4cc27578299d8deb8f868c9eafd008ce807ff13b/packages/frontend/src/pages/rendering/instructions/test.tsx
--------------------------------------------------------------------------------
/packages/frontend/src/pages/scene/SceneStats.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useDevtoolStore } from '../../App';
3 | import { CollapsibleSection } from '../../components/collapsible/collapsible-section';
4 | import { CanvasStatComponent } from '../../components/smooth-charts/stat';
5 | import { useTheme } from '../../components/theme-provider';
6 |
7 | export const SceneStats: React.FC = () => {
8 | const { theme } = useTheme();
9 | const [stats, setStats] = useState>({});
10 | const savedStats = useDevtoolStore.use.stats();
11 |
12 | useEffect(() => {
13 | const updatedStats = { ...stats, ...savedStats };
14 | setStats(updatedStats);
15 | // eslint-disable-next-line react-hooks/exhaustive-deps
16 | }, [savedStats]);
17 |
18 | return (
19 |
20 |
21 |
22 | {stats &&
23 | Object.keys(stats!).map((key) => {
24 | const item = stats![key as keyof typeof stats];
25 | return (
26 |
27 |
35 |
36 | );
37 | })}
38 |
39 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/scene/graph/tree/context-menu-button.tsx:
--------------------------------------------------------------------------------
1 | import type { ContextMenuButtonMetadata } from '@pixi/devtools';
2 | import { useCallback } from 'react';
3 | import type { NodeApi } from 'react-arborist';
4 | import { ContextMenuItem, ContextMenuSeparator } from '../../../../components/ui/context-menu';
5 | import type { BridgeFn } from '../../../../lib/utils';
6 | import type { SceneGraphEntry } from '../../../../types';
7 |
8 | export const NodeContextMenuItem: React.FC<{
9 | title: string;
10 | onClick: React.MouseEventHandler;
11 | isLast?: boolean;
12 | }> = ({ title, onClick, isLast }) => {
13 | isLast = isLast ?? false;
14 | return (
15 | <>
16 | {title}
17 | {!isLast && }
18 | >
19 | );
20 | };
21 |
22 | export const CustomNodeContextMenuItem: React.FC<{
23 | node: NodeApi;
24 | item: ContextMenuButtonMetadata;
25 | bridge: BridgeFn;
26 | isLast?: boolean;
27 | }> = ({ node, item, bridge, isLast }) => {
28 | const handleClick = useCallback(() => {
29 | bridge(
30 | `window.__PIXI_DEVTOOLS_WRAPPER__?.scene.tree.nodeContextMenu(${JSON.stringify(node.id)}, ${JSON.stringify(item.name)})`,
31 | );
32 | }, [node, item, bridge]);
33 |
34 | return ;
35 | };
36 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/scene/graph/tree/cursor.tsx:
--------------------------------------------------------------------------------
1 | import type { CursorProps } from 'react-arborist';
2 |
3 | export function Cursor({ top, left, indent }: CursorProps) {
4 | return (
5 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/scene/graph/tree/node.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import type { NodeRendererProps } from 'react-arborist';
3 | import { useDevtoolStore } from '../../../../App';
4 | import { ContextMenu, ContextMenuContent, ContextMenuTrigger } from '../../../../components/ui/context-menu';
5 | import type { SceneGraphEntry } from '../../../../types';
6 | import { CustomNodeContextMenuItem, NodeContextMenuItem } from './context-menu-button';
7 | import { NodeTrigger } from './node-trigger';
8 |
9 | export function Node({ node, style, dragHandle }: NodeRendererProps) {
10 | const bridge = useDevtoolStore.use.bridge()!;
11 |
12 | const onToggle = useCallback(() => {
13 | node.isInternal && node.toggle();
14 | }, [node]);
15 |
16 | const onSelected = useCallback(() => {
17 | node.tree.select(node);
18 | }, [node]);
19 |
20 | const onDeleted = useCallback(() => {
21 | if (!node.parent || node.data.metadata.locked) return;
22 | node.tree.delete(node);
23 | }, [node]);
24 |
25 | const onRename = useCallback(() => {
26 | setTimeout(() => {
27 | if (node.data.metadata.locked) return;
28 | node.tree.edit(node);
29 | }, 200);
30 | }, [node]);
31 |
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {node.data.metadata.contextMenu?.map((item, i) => (
43 |
50 | ))}
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/packages/frontend/src/pages/scene/scene.ts:
--------------------------------------------------------------------------------
1 | import type { ZustSet } from '../../lib/utils';
2 | import type { ALPHA_MODES, TEXTURE_DIMENSIONS, TEXTURE_FORMATS } from 'pixi.js';
3 | import type { RemoveSetters } from '../../types';
4 | import type { PropertyPanelData } from '../../components/properties/propertyTypes';
5 |
6 | export interface SceneDataState {
7 | gpuSize: number;
8 | pixelWidth: number;
9 | pixelHeight: number;
10 | width: number;
11 | height: number;
12 | mipLevelCount: number;
13 | autoGenerateMipmaps: boolean;
14 | format: TEXTURE_FORMATS;
15 | dimension: TEXTURE_DIMENSIONS;
16 | alphaMode: ALPHA_MODES;
17 | antialias: boolean;
18 | destroyed: boolean;
19 | isPowerOfTwo: boolean;
20 | autoGarbageCollect: boolean;
21 | blob: string | null;
22 | isLoaded: boolean;
23 | name: string;
24 | }
25 |
26 | export interface SceneState {
27 | stats: Record | null;
28 | setStats: (stats: SceneState['stats']) => void;
29 |
30 | selectedNode: string | null;
31 | setSelectedNode: (nodeId: SceneState['selectedNode']) => void;
32 |
33 | activeProps: PropertyPanelData[];
34 | setActiveProps: (props: SceneState['activeProps']) => void;
35 |
36 | overlayPickerEnabled: boolean;
37 | setOverlayPickerEnabled: (enabled: SceneState['overlayPickerEnabled']) => void;
38 |
39 | overlayHighlightEnabled: boolean;
40 | setOverlayHighlightEnabled: (enabled: SceneState['overlayHighlightEnabled']) => void;
41 | }
42 |
43 | export const sceneStateSlice = (set: ZustSet) => ({
44 | setStats: (stats: SceneState['stats']) => set({ stats: stats }),
45 | setSelectedNode: (nodeId: SceneState['selectedNode']) => set({ selectedNode: nodeId }),
46 | setActiveProps: (props: SceneState['activeProps']) => set({ activeProps: props }),
47 | setOverlayPickerEnabled: (enabled: SceneState['overlayPickerEnabled']) => set({ overlayPickerEnabled: enabled }),
48 | setOverlayHighlightEnabled: (enabled: SceneState['overlayHighlightEnabled']) =>
49 | set({ overlayHighlightEnabled: enabled }),
50 | });
51 |
52 | export const sceneStateSelectors: RemoveSetters = {
53 | stats: null,
54 | selectedNode: null,
55 | activeProps: [],
56 | overlayPickerEnabled: false,
57 | overlayHighlightEnabled: true,
58 | };
59 |
--------------------------------------------------------------------------------
/packages/frontend/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { BridgeFn } from './lib/utils';
2 | import type { TextureState } from './pages/assets/assets';
3 | import type { PermanentRenderingStateKeys, RenderingState } from './pages/rendering/rendering';
4 | import type { SceneState } from './pages/scene/scene';
5 | import type { ButtonMetadata, PixiMetadata } from '@pixi/devtools';
6 |
7 | export enum DevtoolMessage {
8 | active = 'pixi-active',
9 | inactive = 'pixi-inactive',
10 | stateUpdate = 'pixi-state-update',
11 | pulse = 'pixi-pulse',
12 |
13 | panelShown = 'devtool:panelShown',
14 | panelHidden = 'devtool:panelHidden',
15 |
16 | pageReload = 'devtool:pageReload',
17 |
18 | overlayStateUpdate = 'pixi-overlay-state-update',
19 | }
20 |
21 | export type SceneGraphEntry = {
22 | id: string;
23 | name: string;
24 | metadata: PixiMetadata;
25 | children: SceneGraphEntry[];
26 | };
27 |
28 | export interface GlobalDevtoolState {
29 | active: boolean;
30 | setActive: (active: DevtoolState['active']) => void;
31 | version: string | null;
32 | setVersion: (version: DevtoolState['version']) => void;
33 | }
34 |
35 | export interface DevtoolState extends GlobalDevtoolState, SceneState, TextureState, RenderingState {
36 | chromeProxy: typeof chrome | null;
37 | setChromeProxy: (chromeProxy: DevtoolState['chromeProxy']) => void;
38 |
39 | version: string | null;
40 | setVersion: (version: DevtoolState['version']) => void;
41 |
42 | sceneGraph: SceneGraphEntry | null;
43 | setSceneGraph: (sceneGraph: DevtoolState['sceneGraph']) => void;
44 |
45 | bridge: BridgeFn | null;
46 | setBridge: (bridge: DevtoolState['bridge']) => void;
47 |
48 | sceneTreeData: {
49 | buttons: ButtonMetadata[];
50 | } | null;
51 | setSceneTreeData: (data: DevtoolState['sceneTreeData']) => void;
52 |
53 | reset: () => void;
54 | }
55 |
56 | // Utility type to remove properties starting with 'set'
57 | export type RemoveSetters = {
58 | [K in keyof T as K extends `set${string}` ? never : K]: T[K];
59 | };
60 |
61 | export type DevtoolStateSelectors = Omit<
62 | RemoveSetters,
63 | 'reset' | 'chromeProxy' | 'bridge' | PermanentRenderingStateKeys
64 | >;
65 |
--------------------------------------------------------------------------------
/packages/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/packages/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "paths": {
5 | "@devtool/backend/*": ["packages/backend/src/*"],
6 | "@pixi/devtools": ["packages/api/src/index.ts"]
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/scripts/release.mts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { bump } from './utils/bump.mts';
3 | import { readJSON } from './utils/json.mts';
4 | import { spawn } from './utils/spawn.mts';
5 |
6 | const packages = ['packages/api', 'packages/devtool-chrome'];
7 |
8 | /**
9 | * Bump the version of all packages in the monorepo
10 | * and their dependencies. Replacement for lerna version --exact --force-publish
11 | */
12 | async function main(): Promise {
13 | try {
14 | // Update the version of all packages
15 | for (const pkg of packages) {
16 | const { version, name } = await readJSON<{ version: string; name: string }>(
17 | path.join(process.cwd(), `${pkg}/package.json`),
18 | );
19 | const nextVersion = await bump(version, name);
20 |
21 | if (nextVersion === version) {
22 | continue;
23 | }
24 |
25 | await spawn('npm', ['version', nextVersion, '--no-git-tag-version'], {
26 | cwd: path.join(process.cwd(), pkg),
27 | });
28 | }
29 |
30 | // Commit the changes
31 | await spawn('git', ['commit', '-am', 'chore: bump package versions']);
32 |
33 | const { version } = await readJSON<{ version: string }>(path.join(process.cwd(), 'package.json'));
34 | const nextVersion = await bump(version, 'root');
35 |
36 | // Finish up: update lock, commit and tag the release
37 | await spawn('npm', ['version', nextVersion, '-m', `v${nextVersion}`]);
38 |
39 | // For testing purposes
40 | if (!process.argv.includes('--no-push')) {
41 | await spawn('git', ['push']);
42 | await spawn('git', ['push', '--tags']);
43 | }
44 | } catch (err) {
45 | console.error((err as Error).message);
46 |
47 | process.exit(1);
48 | }
49 | }
50 |
51 | main();
52 |
--------------------------------------------------------------------------------
/scripts/utils/bump.mts:
--------------------------------------------------------------------------------
1 | import inquirer from 'inquirer';
2 | import semver from 'semver';
3 |
4 | interface BumpAnswers {
5 | bump: 'major' | 'minor' | 'patch' | 'custom' | 'skip';
6 | custom: string;
7 | confirmed: boolean;
8 | }
9 |
10 | /**
11 | * Ask the user to do a version bump.
12 | * @param currentVersion - Current version to change from.
13 | * @returns The next version.
14 | */
15 | export const bump = async (currentVersion: string, pkg: string): Promise => {
16 | const { bump, custom, confirmed } = await inquirer.prompt([
17 | {
18 | name: 'bump',
19 | type: 'list',
20 | message: `${pkg}: Release version (currently v${currentVersion}):`,
21 | choices: [
22 | { value: 'patch', name: `Patch (v${semver.inc(currentVersion, 'patch')})` },
23 | { value: 'minor', name: `Minor (v${semver.inc(currentVersion, 'minor')})` },
24 | { value: 'major', name: `Major (v${semver.inc(currentVersion, 'major')})` },
25 | { value: 'custom', name: `Custom version` },
26 | { value: 'skip', name: `Skip version` },
27 | ],
28 | },
29 | {
30 | name: 'custom',
31 | type: 'input',
32 | message: 'What version? (e.g., 1.0.0)',
33 | when: (answers) => answers.bump === 'custom',
34 | },
35 | {
36 | name: 'confirmed',
37 | type: 'confirm',
38 | default: false,
39 | message: ({ bump, custom }) => {
40 | if (bump === 'skip') {
41 | return `Are you sure you want to skip this version?`;
42 | }
43 |
44 | const nextVersion = bump === 'custom' ? custom : semver.inc(currentVersion, bump);
45 |
46 | return `Are you sure you want to release v${nextVersion}?`;
47 | },
48 | },
49 | ]);
50 |
51 | if (!confirmed) {
52 | throw new Error(`Version bump cancelled.`);
53 | }
54 |
55 | if (bump === 'skip') {
56 | return currentVersion;
57 | }
58 |
59 | const nextVersion = bump === 'custom' ? custom : semver.inc(currentVersion, bump);
60 |
61 | // Make sure the version is valid
62 | if (nextVersion === null || semver.valid(nextVersion) === null) {
63 | throw new Error(`Error: Invalid version: ${nextVersion}`);
64 | }
65 |
66 | // Make sure we are incrementing the version
67 | if (semver.lte(nextVersion, currentVersion)) {
68 | throw new Error(`Error: Next version (v${nextVersion}) is less than current version (v${currentVersion}).`);
69 | }
70 |
71 | return nextVersion;
72 | };
73 |
--------------------------------------------------------------------------------
/scripts/utils/json.mts:
--------------------------------------------------------------------------------
1 | import { promises } from 'fs';
2 |
3 | /**
4 | * Read JSON file using async
5 | * @param file
6 | */
7 | const readJSON = async (file: string): Promise => JSON.parse(await promises.readFile(file, 'utf8')) as T;
8 |
9 | /**
10 | * Write JSON file using async
11 | * @param file
12 | * @param data
13 | */
14 | const writeJSON = (file: string, data: any) => promises.writeFile(file, `${JSON.stringify(data, null, 2)}\n`);
15 |
16 | export { readJSON, writeJSON };
17 |
--------------------------------------------------------------------------------
/scripts/utils/spawn.mts:
--------------------------------------------------------------------------------
1 | import childProcess from 'child_process';
2 |
3 | /**
4 | * Utility to do spawn but as a Promise
5 | * @param command
6 | * @param args
7 | * @param options
8 | */
9 | export const spawn = (command: string, args: string[], options: childProcess.SpawnOptions = {}) =>
10 | new Promise((resolve, reject) => {
11 | const child = childProcess.spawn(command, args, {
12 | stdio: 'inherit',
13 | cwd: process.cwd(),
14 | shell: process.platform === 'win32',
15 | ...options,
16 | });
17 |
18 | child.on('close', async (code) => {
19 | if (code === 0) {
20 | resolve();
21 | }
22 | });
23 | child.on('error', reject);
24 | });
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | "moduleResolution": "bundler",
10 | "allowImportingTsExtensions": true,
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "noEmit": true,
14 | "jsx": "react-jsx",
15 |
16 | "strict": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noFallthroughCasesInSwitch": true,
20 |
21 | "noImplicitOverride": true,
22 |
23 | "baseUrl": "./",
24 | "paths": {
25 | "@devtool/frontend/*": ["packages/frontend/src/*"],
26 | "@devtool/backend/*": ["packages/backend/src/*"],
27 | "@pixi/devtools": ["packages/api/src/index.ts"]
28 | }
29 | },
30 | "ts-node": {
31 | "files": true,
32 | "compilerOptions": {
33 | "esModuleInterop": true
34 | },
35 | "transpileOnly": true
36 | }
37 | }
38 |
--------------------------------------------------------------------------------