#
tokens: 32962/50000 26/26 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .github
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .npmrc
├── cli.mjs
├── eslint.config.mjs
├── extension
│   ├── images
│   │   ├── icon128.png
│   │   ├── icon16.png
│   │   └── icon48.png
│   ├── isolated.js
│   ├── main.js
│   ├── manifest.json
│   ├── popup.css
│   ├── popup.html
│   └── popup.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── release.sh
├── src
│   ├── server.ts
│   ├── tools
│   │   ├── asset.ts
│   │   ├── assets
│   │   │   ├── material.ts
│   │   │   └── script.ts
│   │   ├── entity.ts
│   │   ├── scene.ts
│   │   ├── schema
│   │   │   ├── asset.ts
│   │   │   ├── common.ts
│   │   │   ├── entity.ts
│   │   │   └── scene-settings.ts
│   │   └── store.ts
│   └── wss.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------

```
1 | registry=https://registry.npmjs.org/
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
  2 | 
  3 | # Logs
  4 | 
  5 | logs
  6 | _.log
  7 | npm-debug.log_
  8 | yarn-debug.log*
  9 | yarn-error.log*
 10 | lerna-debug.log*
 11 | .pnpm-debug.log*
 12 | 
 13 | # Caches
 14 | 
 15 | .cache
 16 | 
 17 | # Diagnostic reports (https://nodejs.org/api/report.html)
 18 | 
 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
 20 | 
 21 | # Runtime data
 22 | 
 23 | pids
 24 | _.pid
 25 | _.seed
 26 | *.pid.lock
 27 | 
 28 | # Directory for instrumented libs generated by jscoverage/JSCover
 29 | 
 30 | lib-cov
 31 | 
 32 | # Coverage directory used by tools like istanbul
 33 | 
 34 | coverage
 35 | *.lcov
 36 | 
 37 | # nyc test coverage
 38 | 
 39 | .nyc_output
 40 | 
 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 42 | 
 43 | .grunt
 44 | 
 45 | # Bower dependency directory (https://bower.io/)
 46 | 
 47 | bower_components
 48 | 
 49 | # node-waf configuration
 50 | 
 51 | .lock-wscript
 52 | 
 53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 54 | 
 55 | build/Release
 56 | 
 57 | # Dependency directories
 58 | 
 59 | node_modules/
 60 | jspm_packages/
 61 | 
 62 | # Snowpack dependency directory (https://snowpack.dev/)
 63 | 
 64 | web_modules/
 65 | 
 66 | # TypeScript cache
 67 | 
 68 | *.tsbuildinfo
 69 | 
 70 | # Optional npm cache directory
 71 | 
 72 | .npm
 73 | 
 74 | # Optional eslint cache
 75 | 
 76 | .eslintcache
 77 | 
 78 | # Optional stylelint cache
 79 | 
 80 | .stylelintcache
 81 | 
 82 | # Microbundle cache
 83 | 
 84 | .rpt2_cache/
 85 | .rts2_cache_cjs/
 86 | .rts2_cache_es/
 87 | .rts2_cache_umd/
 88 | 
 89 | # Optional REPL history
 90 | 
 91 | .node_repl_history
 92 | 
 93 | # Output of 'npm pack'
 94 | 
 95 | *.tgz
 96 | 
 97 | # Yarn Integrity file
 98 | 
 99 | .yarn-integrity
100 | 
101 | # dotenv environment variable files
102 | 
103 | .env
104 | .env.development.local
105 | .env.test.local
106 | .env.production.local
107 | .env.local
108 | 
109 | # parcel-bundler cache (https://parceljs.org/)
110 | 
111 | .parcel-cache
112 | 
113 | # Next.js build output
114 | 
115 | .next
116 | out
117 | 
118 | # Nuxt.js build / generate output
119 | 
120 | .nuxt
121 | dist
122 | 
123 | # Gatsby files
124 | 
125 | # Comment in the public line in if your project uses Gatsby and not Next.js
126 | 
127 | # https://nextjs.org/blog/next-9-1#public-directory-support
128 | 
129 | # public
130 | 
131 | # vuepress build output
132 | 
133 | .vuepress/dist
134 | 
135 | # vuepress v2.x temp and cache directory
136 | 
137 | .temp
138 | 
139 | # Docusaurus cache and generated files
140 | 
141 | .docusaurus
142 | 
143 | # Serverless directories
144 | 
145 | .serverless/
146 | 
147 | # FuseBox cache
148 | 
149 | .fusebox/
150 | 
151 | # DynamoDB Local files
152 | 
153 | .dynamodb/
154 | 
155 | # TernJS port file
156 | 
157 | .tern-port
158 | 
159 | # Stores VSCode versions used for testing VSCode extensions
160 | 
161 | .vscode-test
162 | 
163 | # yarn v2
164 | 
165 | .yarn/cache
166 | .yarn/unplugged
167 | .yarn/build-state.yml
168 | .yarn/install-state.gz
169 | .pnp.*
170 | 
171 | # IntelliJ based IDEs
172 | .idea
173 | 
174 | # Finder (MacOS) folder config
175 | .DS_Store
176 | 
```

--------------------------------------------------------------------------------
/extension/popup.html:
--------------------------------------------------------------------------------

```html
 1 | <!DOCTYPE html>
 2 | <html>
 3 |     <head>
 4 |         <title>PlayCanvas Editor MCP Extension</title>
 5 |         <link rel="stylesheet" href="popup.css" />
 6 |         <script src="popup.js" type="module"></script>
 7 |     </head>
 8 |     <body></body>
 9 | </html>
10 | 
```

--------------------------------------------------------------------------------
/extension/isolated.js:
--------------------------------------------------------------------------------

```javascript
 1 | window.addEventListener('message', (event) => {
 2 |     if (event.data?.ctx !== 'main') {
 3 |         return;
 4 |     }
 5 |     const { name, args } = event.data;
 6 |     chrome.runtime.sendMessage({ name, args });
 7 | });
 8 | chrome.runtime.onMessage.addListener((data) => {
 9 |     const { name, args } = data;
10 |     window.postMessage({ name, args, ctx: 'isolated' });
11 | });
12 | 
```

--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash -e
 2 | 
 3 | TYPE=$1
 4 | 
 5 | if [ -z "$TYPE" ]; then
 6 |     echo "Usage: $0 <type>"
 7 |     echo "type: major, minor, patch"
 8 |     exit 1
 9 | fi
10 | 
11 | # Confirm release
12 | read -p "Are you sure you want to release a new version? (y/N): " -r
13 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then
14 |     echo "Release cancelled."
15 |     exit 1
16 | fi
17 | 
18 | # Tag release
19 | npm version $TYPE
20 | 
21 | # Publish to npm
22 | npm publish
23 | 
24 | # Push to GitHub
25 | git push origin main
26 | git push --tags
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: CI
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ main ]
 6 |   pull_request:
 7 |     branches: [ main ]
 8 | 
 9 | jobs:
10 |   lint:
11 |     name: Lint JS
12 |     runs-on: ubuntu-latest
13 |     steps:
14 |     - name: Checkout code
15 |       uses: actions/checkout@v2
16 |     - name: Setup Node.js 18.20.0
17 |       uses: actions/[email protected]
18 |       with:
19 |         node-version: 18.20.0
20 |     - name: Install dependencies
21 |       run: npm ci
22 |     - name: Run ESLint
23 |       run: npm run lint
24 | 
```

--------------------------------------------------------------------------------
/src/tools/assets/material.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | 
 3 | import { type WSS } from '../../wss';
 4 | import { AssetIdSchema, RgbSchema } from '../schema/common';
 5 | 
 6 | export const register = (mcp: McpServer, wss: WSS) => {
 7 |     mcp.tool(
 8 |         'set_material_diffuse',
 9 |         'Set diffuse property on a material',
10 |         {
11 |             assetId: AssetIdSchema,
12 |             color: RgbSchema
13 |         },
14 |         ({ assetId, color }) => {
15 |             return wss.call('assets:property:set', assetId, 'diffuse', color);
16 |         }
17 |     );
18 | };
19 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     // Enable latest features
 4 |     "lib": ["ESNext", "DOM"],
 5 |     "types": ["node", "chrome"],
 6 |     "target": "ESNext",
 7 |     "module": "ESNext",
 8 |     "moduleDetection": "force",
 9 |     "allowJs": true,
10 | 
11 |     // Bundler mode
12 |     "moduleResolution": "bundler",
13 |     "allowImportingTsExtensions": true,
14 |     "verbatimModuleSyntax": true,
15 |     "noEmit": true,
16 | 
17 |     // Best practices
18 |     "strict": true,
19 |     "skipLibCheck": true,
20 |     "noFallthroughCasesInSwitch": true,
21 | 
22 |     // Some stricter flags (disabled by default)
23 |     "noUnusedLocals": false,
24 |     "noUnusedParameters": false,
25 |     "noPropertyAccessFromIndexSignature": false
26 |   }
27 | }
28 | 
```

--------------------------------------------------------------------------------
/src/tools/scene.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | 
 3 | import { type WSS } from '../wss';
 4 | import { SceneSettingsSchema } from './schema/scene-settings';
 5 | 
 6 | export const register = (mcp: McpServer, wss: WSS) => {
 7 |     mcp.tool(
 8 |         'modify_scene_settings',
 9 |         'Modify the scene settings',
10 |         {
11 |             settings: SceneSettingsSchema
12 |         },
13 |         ({ settings }) => {
14 |             return wss.call('scene:settings:modify', settings);
15 |         }
16 |     );
17 | 
18 |     mcp.tool(
19 |         'query_scene_settings',
20 |         'Query the scene settings',
21 |         {},
22 |         () => {
23 |             return wss.call('scene:settings:modify', {});
24 |         }
25 |     );
26 | };
27 | 
```

--------------------------------------------------------------------------------
/src/tools/assets/script.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | import { z } from 'zod';
 3 | 
 4 | import { type WSS } from '../../wss';
 5 | 
 6 | export const register = (mcp: McpServer, wss: WSS) => {
 7 |     mcp.tool(
 8 |         'set_script_text',
 9 |         'Set script text',
10 |         {
11 |             assetId: z.number(),
12 |             text: z.string()
13 |         },
14 |         ({ assetId, text }) => {
15 |             return wss.call('assets:script:text:set', assetId, text);
16 |         }
17 |     );
18 | 
19 |     mcp.tool(
20 |         'script_parse',
21 |         'Parse the script after modification',
22 |         {
23 |             assetId: z.number()
24 |         },
25 |         ({ assetId }) => {
26 |             return wss.call('assets:script:parse', assetId);
27 |         }
28 |     );
29 | };
30 | 
```

--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------

```
 1 | import playcanvasConfig from '@playcanvas/eslint-config';
 2 | import typescriptParser from '@typescript-eslint/parser';
 3 | import globals from 'globals';
 4 | 
 5 | export default [
 6 |     ...playcanvasConfig,
 7 |     {
 8 |         files: ['**/*.ts', '**/*.mjs', '**/*.js'],
 9 |         languageOptions: {
10 |             ecmaVersion: 2022,
11 |             sourceType: 'module',
12 |             parser: typescriptParser,
13 |             parserOptions: {
14 |                 requireConfigFile: false
15 |             },
16 |             globals: {
17 |                 ...globals.browser,
18 |                 ...globals.mocha,
19 |                 ...globals.node,
20 |                 ...globals.webextensions
21 |             }
22 |         },
23 |         settings: {
24 |             'import/resolver': {
25 |                 typescript: {
26 |                     alwaysTryTypes: true,
27 |                     project: './tsconfig.json'
28 |                 }
29 |             }
30 |         }
31 |     }
32 | ];
33 | 
```

--------------------------------------------------------------------------------
/extension/manifest.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |     "manifest_version": 3,
 3 |     "name": "PlayCanvas Editor MCP Extension",
 4 |     "description": "This extension allows the MCP Server to communicate with the PlayCanvas Editor.",
 5 |     "icons": {
 6 |         "16": "images/icon16.png",
 7 |         "48": "images/icon48.png",
 8 |         "128": "images/icon128.png"
 9 |     },
10 |     "version": "1.0",
11 |     "permissions": ["activeTab"],
12 |     "action": {
13 |         "default_popup": "popup.html"
14 |     },
15 |     "host_permissions": ["http://playcanvas.com/*", "https://playcanvas.com/*"],
16 |     "content_scripts": [
17 |         {
18 |             "matches": ["http://playcanvas.com/editor*", "https://playcanvas.com/editor*"],
19 |             "js": ["main.js"],
20 |             "world": "MAIN"
21 |         },
22 |         {
23 |             "matches": ["http://playcanvas.com/editor*", "https://playcanvas.com/editor*"],
24 |             "js": ["isolated.js"],
25 |             "world": "ISOLATED"
26 |         }
27 |     ]
28 | }
29 | 
```

--------------------------------------------------------------------------------
/src/tools/schema/common.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | 
 3 | export const RgbSchema = z.tuple([
 4 |     z.number().min(0).max(1).describe('Red'),
 5 |     z.number().min(0).max(1).describe('Green'),
 6 |     z.number().min(0).max(1).describe('Blue')
 7 | ]).describe('A 3-channel RGB color');
 8 | 
 9 | export const RgbaSchema = z.tuple([
10 |     z.number().min(0).max(1).describe('Red'),
11 |     z.number().min(0).max(1).describe('Green'),
12 |     z.number().min(0).max(1).describe('Blue'),
13 |     z.number().min(0).max(1).describe('Alpha')
14 | ]).describe('A 4-channel RGBA color');
15 | 
16 | export const Vec2Schema = z.tuple([
17 |     z.number().describe('X'),
18 |     z.number().describe('Y')
19 | ]).describe('A 2D vector');
20 | 
21 | export const Vec3Schema = z.tuple([
22 |     z.number().describe('X'),
23 |     z.number().describe('Y'),
24 |     z.number().describe('Z')
25 | ]).describe('A 3D vector');
26 | 
27 | export const Vec4Schema = z.tuple([
28 |     z.number().describe('X'),
29 |     z.number().describe('Y'),
30 |     z.number().describe('Z'),
31 |     z.number().describe('W')
32 | ]).describe('A 4D vector');
33 | 
34 | export const AssetIdSchema = z.number().int().nullable().describe('An asset ID.');
35 | export const EntityIdSchema = z.string().uuid().describe('An entity ID.');
36 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@playcanvas/editor-mcp-server",
 3 |   "version": "0.0.2",
 4 |   "author": "PlayCanvas <[email protected]>",
 5 |   "homepage": "https://github.com/playcanvas/editor-mcp-server#readme",
 6 |   "description": "The PlayCanvas Editor MCP Server",
 7 |   "keywords": [
 8 |     "playcanvas",
 9 |     "editor",
10 |     "mcp",
11 |     "cli"
12 |   ],
13 |   "license": "MIT",
14 |   "type": "module",
15 |   "bugs": {
16 |     "url": "https://github.com/playcanvas/editor-mcp-server/issues"
17 |   },
18 |   "repository": {
19 |     "type": "git",
20 |     "url": "git+https://github.com/playcanvas/editor-mcp-server.git"
21 |   },
22 |   "files": [
23 |     "src"
24 |   ],
25 |   "bin": {
26 |     "mcp-server": "cli.mjs"
27 |   },
28 |   "scripts": {
29 |     "start": "tsx src/server.ts",
30 |     "watch": "tsx watch src/server.ts",
31 |     "debug": "npx @modelcontextprotocol/inspector tsx src/server.ts",
32 |     "lint": "eslint src"
33 |   },
34 |   "devDependencies": {
35 |     "@playcanvas/eslint-config": "^2.1.0",
36 |     "@types/chrome": "^0.1.12",
37 |     "@types/command-line-args": "^5.2.3",
38 |     "@types/command-line-usage": "^5.0.4",
39 |     "@types/ws": "^8.18.1",
40 |     "@typescript-eslint/parser": "^8.44.1",
41 |     "eslint": "^9.36.0",
42 |     "eslint-import-resolver-typescript": "^4.4.4",
43 |     "globals": "^16.4.0",
44 |     "typescript": "^5.9.2"
45 |   },
46 |   "dependencies": {
47 |     "@modelcontextprotocol/sdk": "^1.18.1",
48 |     "command-line-args": "^6.0.1",
49 |     "command-line-usage": "^7.0.3",
50 |     "tsx": "^4.20.5",
51 |     "ws": "^8.18.3",
52 |     "zod": "^4.1.11"
53 |   }
54 | }
55 | 
```

--------------------------------------------------------------------------------
/cli.mjs:
--------------------------------------------------------------------------------

```
 1 | #!/usr/bin/env node
 2 | 
 3 | import { execSync } from 'child_process';
 4 | import { readFileSync } from 'fs';
 5 | import { dirname, resolve } from 'path';
 6 | import { fileURLToPath } from 'url';
 7 | 
 8 | import commandLineArgs from 'command-line-args';
 9 | import commandLineUsage from 'command-line-usage';
10 | 
11 | const __filename = fileURLToPath(import.meta.url);
12 | const __dirname = dirname(__filename);
13 | 
14 | const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8'));
15 | 
16 | const options = [
17 |     {
18 |         name: 'help',
19 |         alias: 'h',
20 |         type: Boolean,
21 |         defaultValue: false
22 |     },
23 |     {
24 |         name: 'version',
25 |         alias: 'v',
26 |         type: Boolean,
27 |         defaultValue: false
28 |     },
29 |     {
30 |         name: 'port',
31 |         alias: 'p',
32 |         type: Number,
33 |         defaultValue: 52000
34 |     }
35 | ];
36 | 
37 | const help = commandLineUsage([{
38 |     header: 'Usage',
39 |     content: `npx ${pkg.name} [options]`
40 | }, {
41 |     header: 'Options',
42 |     optionList: options
43 | }]);
44 | 
45 | const main = (argv) => {
46 |     const args = commandLineArgs(options, { argv });
47 |     if (args.help) {
48 |         console.log(help);
49 |         return;
50 |     }
51 |     if (args.version) {
52 |         console.log(`v${pkg.version}`);
53 |         return;
54 |     }
55 | 
56 |     process.env.PORT = args.port.toString();
57 |     try {
58 |         execSync(`npx tsx ${resolve(__dirname, 'src', 'server.ts')}`, {
59 |             stdio: 'inherit',
60 |             env: process.env
61 |         });
62 |     } catch (error) {
63 |         console.error('[CLI ERROR]', error.message);
64 |         process.exit(1);
65 |     }
66 | };
67 | 
68 | main(process.argv.slice(2));
69 | 
```

--------------------------------------------------------------------------------
/src/tools/store.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | import { z } from 'zod';
 3 | 
 4 | import { type WSS } from '../wss';
 5 | 
 6 | const orderEnum = {
 7 |     'asc': 1,
 8 |     'desc': -1
 9 | };
10 | 
11 | export const register = (mcp: McpServer, wss: WSS) => {
12 |     mcp.tool(
13 |         'store_search',
14 |         'Search for an asset in the store',
15 |         {
16 |             // store: z.enum(['playcanvas', 'sketchfab']).optional(),
17 |             search: z.string(),
18 |             order: z.enum(['asc', 'desc']).optional(),
19 |             skip: z.number().optional(),
20 |             limit: z.number().optional()
21 |         },
22 |         ({ search, order, skip, limit }) => {
23 |             return wss.call('store:playcanvas:list', {
24 |                 search,
25 |                 order: order ? orderEnum[order] : undefined,
26 |                 skip,
27 |                 limit
28 |             });
29 |         }
30 |     );
31 | 
32 |     mcp.tool(
33 |         'store_get',
34 |         'Get an asset from the store',
35 |         {
36 |             // store: z.enum(['playcanvas', 'sketchfab']).optional(),
37 |             id: z.string()
38 |         },
39 |         ({ id }) => {
40 |             return wss.call('store:playcanvas:get', id);
41 |         }
42 |     );
43 | 
44 |     mcp.tool(
45 |         'store_download',
46 |         'Download an asset from the store',
47 |         {
48 |             // store: z.enum(['playcanvas', 'sketchfab']).optional(),
49 |             id: z.string(),
50 |             name: z.string(),
51 |             license: z.object({
52 |                 author: z.string(),
53 |                 authorUrl: z.string().url(),
54 |                 license: z.string()
55 |             })
56 |         },
57 |         ({ id, name, license }) => {
58 |             return wss.call('store:playcanvas:clone', id, name, license);
59 |         }
60 |     );
61 | };
62 | 
```

--------------------------------------------------------------------------------
/extension/popup.css:
--------------------------------------------------------------------------------

```css
  1 | :root {
  2 |     --color-primary: #364346;
  3 |     --color-secondary: #2c393c;
  4 |     --color-text: #b1b8ba;
  5 |     --color-hover: rgba(255,102,0,.3);
  6 |     --color-red: #e74c3c;
  7 |     --color-orange: #f1c40f;
  8 |     --color-green: #2ecc71;
  9 | }
 10 | 
 11 | * {
 12 |     font-family: 'Proxima Nova Regular", "Helvetica Neue", Arial, Helvetica, sans-serif;
 13 | }
 14 | 
 15 | html, body {
 16 |     margin: 0;
 17 |     width: 300px;
 18 |     height: 100%;
 19 | }
 20 | 
 21 | #root {
 22 |     background-color: var(--color-primary);
 23 |     display: flex;
 24 |     flex-direction: column;
 25 |     width: 100%;
 26 |     height: 100%;
 27 | }
 28 | 
 29 | .header {
 30 |     text-transform: uppercase;
 31 |     font-size: 12px;
 32 |     color: white;
 33 |     font-weight: bold;
 34 |     background-color: var(--color-secondary);
 35 |     padding: 10px;
 36 | }
 37 | 
 38 | .body {
 39 |     padding: 10px;
 40 |     display: flex;
 41 |     flex-direction: column;
 42 |     gap: 10px;
 43 | }
 44 | 
 45 | .input {
 46 |     background-color: var(--color-secondary);
 47 |     border: 1px solid #293538;
 48 |     border-radius: 2px;
 49 |     box-sizing: border-box;
 50 |     color: var(--color-text);
 51 |     font-family: inconsolatamedium, Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace;
 52 |     font-size: 12px;
 53 |     padding: 6px;
 54 |     outline: none;
 55 |     width: 100%;
 56 | }
 57 | .input:hover {
 58 |     box-shadow: 0 0 2px 1px var(--color-hover);
 59 | }
 60 | .input.disabled {
 61 |     opacity: 0.4;
 62 |     cursor: not-allowed;
 63 | }
 64 | .input.disabled:hover {
 65 |     box-shadow: none;
 66 | }
 67 | 
 68 | .button {
 69 |     background-color: var(--color-secondary);
 70 |     border: 1px solid #20292b;
 71 |     border-radius: 2px;
 72 |     color: var(--color-text);
 73 |     cursor: pointer;
 74 |     font-size: 12px;
 75 |     padding: 5px;
 76 | }
 77 | .button:hover {
 78 |     color: white;
 79 |     box-shadow: 0 0 2px 1px var(--color-hover);
 80 | }
 81 | .button.disabled {
 82 |     opacity: 0.4;
 83 |     cursor: not-allowed;
 84 | }
 85 | .button.disabled:hover {
 86 |     color: var(--color-text);
 87 |     box-shadow: none;
 88 | }
 89 | 
 90 | .group {
 91 |     display: flex;
 92 |     gap: 10px;
 93 | }
 94 | 
 95 | .indicator {
 96 |     background-color: var(--color-red);
 97 |     border-radius: 50%;
 98 |     width: 14px;
 99 |     height: 14px;
100 | }
101 | .indicator.connecting {
102 |     background-color: var(--color-orange);
103 | }
104 | .indicator.connected {
105 |     background-color: var(--color-green);
106 | }
107 | 
108 | .label {
109 |     color: var(--color-text);
110 |     font-size: 12px;
111 |     line-height: 14px;
112 |     user-select: none;
113 | }
```

--------------------------------------------------------------------------------
/src/tools/asset.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | import { z } from 'zod';
 3 | 
 4 | import { type WSS } from '../wss';
 5 | import { CssCreateSchema, FolderCreateSchema, HtmlCreateSchema, MaterialCreateSchema, ScriptCreateSchema, ShaderCreateSchema, TemplateCreateSchema, TextCreateSchema } from './schema/asset';
 6 | import { AssetIdSchema } from './schema/common';
 7 | 
 8 | export const register = (mcp: McpServer, wss: WSS) => {
 9 |     mcp.tool(
10 |         'create_assets',
11 |         'Create one or more assets',
12 |         {
13 |             assets: z.array(
14 |                 z.union([
15 |                     CssCreateSchema,
16 |                     FolderCreateSchema,
17 |                     HtmlCreateSchema,
18 |                     MaterialCreateSchema,
19 |                     ScriptCreateSchema,
20 |                     ShaderCreateSchema,
21 |                     TemplateCreateSchema,
22 |                     TextCreateSchema
23 |                 ])
24 |             ).nonempty().describe('Array of assets to create.')
25 |         },
26 |         ({ assets }) => {
27 |             return wss.call('assets:create', assets);
28 |         }
29 |     );
30 | 
31 |     mcp.tool(
32 |         'list_assets',
33 |         'List all assets with the option to filter by type',
34 |         {
35 |             type: z.enum(['css', 'cubemap', 'folder', 'font', 'html', 'json', 'material', 'render', 'script', 'shader', 'template', 'text', 'texture']).optional().describe('The type of assets to list. If not specified, all assets will be listed.')
36 |         },
37 |         ({ type }) => {
38 |             return wss.call('assets:list', type);
39 |         }
40 |     );
41 | 
42 |     mcp.tool(
43 |         'delete_assets',
44 |         'Delete one or more assets',
45 |         {
46 |             ids: z.array(AssetIdSchema).nonempty().describe('The asset IDs of the assets to delete')
47 |         },
48 |         ({ ids }) => {
49 |             return wss.call('assets:delete', ids);
50 |         }
51 |     );
52 | 
53 |     mcp.tool(
54 |         'instantiate_template_assets',
55 |         'Instantiate one or more template assets',
56 |         {
57 |             ids: z.array(AssetIdSchema).nonempty().describe('The asset IDs of the template assets to instantiate')
58 |         },
59 |         ({ ids }) => {
60 |             return wss.call('assets:instantiate', ids);
61 |         }
62 |     );
63 | };
64 | 
```

--------------------------------------------------------------------------------
/src/wss.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebSocketServer, WebSocket } from 'ws';
  2 | 
  3 | const PING_DELAY = 1000;
  4 | 
  5 | class WSS {
  6 |     private _server: WebSocketServer;
  7 | 
  8 |     private _socket?: WebSocket;
  9 | 
 10 |     private _callbacks = new Map();
 11 | 
 12 |     private _id = 0;
 13 | 
 14 |     private _pingInterval: ReturnType<typeof setInterval> | null = null;
 15 | 
 16 |     constructor(port: number) {
 17 |         this._server = new WebSocketServer({ port });
 18 |         console.error('[WSS] Listening on port', port);
 19 |         this._waitForSocket();
 20 |     }
 21 | 
 22 |     private _waitForSocket() {
 23 |         this._server.on('connection', (ws) => {
 24 |             if (this._socket) {
 25 |                 return;
 26 |             }
 27 |             console.error('[WSS] Connected');
 28 |             ws.on('message', (data) => {
 29 |                 try {
 30 |                     const { id, res } = JSON.parse(data.toString());
 31 |                     if (this._callbacks.has(id)) {
 32 |                         this._callbacks.get(id)(res);
 33 |                         this._callbacks.delete(id);
 34 |                     }
 35 |                 } catch (e) {
 36 |                     console.error('[WSS]', e);
 37 |                 }
 38 |             });
 39 |             ws.on('close', (_code, reason) => {
 40 |                 console.error('[WSS] Disconnected');
 41 |                 this._socket = undefined;
 42 |                 if (reason.toString() !== 'FORCE') {
 43 |                     this._waitForSocket();
 44 |                 }
 45 |             });
 46 | 
 47 |             this._socket = ws;
 48 | 
 49 |             if (this._pingInterval) {
 50 |                 clearInterval(this._pingInterval);
 51 |             }
 52 |             this._pingInterval = setInterval(() => {
 53 |                 this.call('ping').then(() => console.error('[WSS] Ping'));
 54 |             }, PING_DELAY);
 55 |         });
 56 |     }
 57 | 
 58 |     private _send(name: string, ...args: any[]) {
 59 |         return new Promise<{ data?: any, error?: string }>((resolve, reject) => {
 60 |             const id = this._id++;
 61 |             this._callbacks.set(id, resolve);
 62 |             if (!this._socket) {
 63 |                 reject(new Error('No socket'));
 64 |                 return;
 65 |             }
 66 |             if (this._socket.readyState !== WebSocket.OPEN) {
 67 |                 reject(new Error('Socket not open'));
 68 |                 return;
 69 |             }
 70 |             this._socket.send(JSON.stringify({ id, name, args }));
 71 |         });
 72 |     }
 73 | 
 74 |     async call(name: string, ...args: any[]): Promise<{ content: any[], isError?: boolean }> {
 75 |         try {
 76 |             const { data, error } = await this._send(name, ...args);
 77 |             if (error) {
 78 |                 throw new Error(error);
 79 |             }
 80 |             return {
 81 |                 content: [{
 82 |                     type: 'text',
 83 |                     text: JSON.stringify(data)
 84 |                 }]
 85 |             };
 86 |         } catch (err: any) {
 87 |             return {
 88 |                 content: [{
 89 |                     type: 'text',
 90 |                     text: err.message
 91 |                 }],
 92 |                 isError: true
 93 |             };
 94 |         }
 95 |     }
 96 | 
 97 |     close() {
 98 |         if (this._pingInterval) {
 99 |             clearInterval(this._pingInterval);
100 |         }
101 |         if (this._socket) {
102 |             this._socket.close(1000, 'FORCE');
103 |         }
104 |         this._server.close();
105 |         console.error('[WSS] Closed');
106 |     }
107 | }
108 | 
109 | export { WSS };
110 | 
```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { execSync } from 'child_process';
  2 | 
  3 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  4 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  5 | import { ListPromptsRequestSchema, ListResourcesRequestSchema } from '@modelcontextprotocol/sdk/types.js';
  6 | 
  7 | import { register as registerAsset } from './tools/asset';
  8 | import { register as registerAssetMaterial } from './tools/assets/material';
  9 | import { register as registerAssetScript } from './tools/assets/script';
 10 | import { register as registerEntity } from './tools/entity';
 11 | import { register as registerScene } from './tools/scene';
 12 | import { register as registerStore } from './tools/store';
 13 | import { WSS } from './wss';
 14 | 
 15 | const PORT = parseInt(process.env.PORT || '52000', 10);
 16 | 
 17 | const poll = (cond: () => boolean, rate: number = 1000) => {
 18 |     return new Promise<void>((resolve) => {
 19 |         const id = setInterval(() => {
 20 |             if (cond()) {
 21 |                 clearInterval(id);
 22 |                 resolve();
 23 |             }
 24 |         }, rate);
 25 |     });
 26 | };
 27 | 
 28 | const findPid = (port: number) => {
 29 |     if (process.platform === 'win32') {
 30 |         try {
 31 |             return execSync(`netstat -ano | findstr 0.0.0.0:${PORT}`).toString().trim().split(' ').pop();
 32 |         } catch (e) {
 33 |             return '';
 34 |         }
 35 |     }
 36 |     return execSync(`lsof -i :${port} | grep LISTEN | awk '{print $2}'`).toString().trim();
 37 | };
 38 | 
 39 | const kill = (pid: string) => {
 40 |     if (process.platform === 'win32') {
 41 |         try {
 42 |             execSync(`taskkill /F /PID ${pid}`);
 43 |         } catch (e) {
 44 |             // Ignore
 45 |         }
 46 |         return;
 47 |     }
 48 |     execSync(`kill -9 ${pid}`);
 49 | };
 50 | 
 51 | // Kill the existing server
 52 | const pid = findPid(PORT);
 53 | if (pid) {
 54 |     kill(pid);
 55 | }
 56 | 
 57 | // Wait for the server to stop
 58 | await poll(() => !findPid(PORT));
 59 | 
 60 | // Create a WebSocket server
 61 | const wss = new WSS(PORT);
 62 | 
 63 | // Create an MCP server
 64 | const mcp = new McpServer({
 65 |     name: 'PlayCanvas',
 66 |     version: '1.0.0'
 67 | }, {
 68 |     capabilities: {
 69 |         tools: {},
 70 |         resources: {},
 71 |         prompts: {}
 72 |     }
 73 | });
 74 | mcp.server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [] }));
 75 | mcp.server.setRequestHandler(ListPromptsRequestSchema, () => ({ prompts: [] }));
 76 | 
 77 | // Tools
 78 | registerEntity(mcp, wss);
 79 | registerAsset(mcp, wss);
 80 | registerAssetMaterial(mcp, wss);
 81 | registerAssetScript(mcp, wss);
 82 | registerScene(mcp, wss);
 83 | registerStore(mcp, wss);
 84 | 
 85 | // Start receiving messages on stdin and sending messages on stdout
 86 | const transport = new StdioServerTransport();
 87 | mcp.connect(transport).then(() => {
 88 |     console.error('[MCP] Listening');
 89 | }).catch((e) => {
 90 |     console.error('[MCP] Error', e);
 91 |     process.exit(1);
 92 | });
 93 | 
 94 | const close = () => {
 95 |     mcp.close().finally(() => {
 96 |         console.error('[MCP] Closed');
 97 |         wss.close();
 98 |         process.exit(0);
 99 |     });
100 | };
101 | 
102 | // Handle uncaught exceptions and unhandled rejections
103 | process.on('uncaughtException', (err) => {
104 |     console.error('[process] Uncaught exception', err);
105 | });
106 | process.on('unhandledRejection', (reason) => {
107 |     console.error('[process] Unhandled rejection', reason);
108 | });
109 | 
110 | // Clean up on exit
111 | process.stdin.on('close', () => {
112 |     console.error('[process] stdin closed');
113 |     close();
114 | });
115 | process.on('SIGINT', () => {
116 |     console.error('[process] SIGINT');
117 |     close();
118 | });
119 | process.on('SIGTERM', () => {
120 |     console.error('[process] SIGTERM');
121 |     close();
122 | });
123 | process.on('SIGQUIT', () => {
124 |     console.error('[process] SIGQUIT');
125 |     close();
126 | });
127 | 
```

--------------------------------------------------------------------------------
/src/tools/entity.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  2 | import { z } from 'zod';
  3 | 
  4 | import { type WSS } from '../wss';
  5 | import { EntityIdSchema } from './schema/common';
  6 | import { ComponentsSchema, ComponentNameSchema, EntitySchema } from './schema/entity';
  7 | 
  8 | export const register = (mcp: McpServer, wss: WSS) => {
  9 |     mcp.tool(
 10 |         'create_entities',
 11 |         'Create one or more entities',
 12 |         {
 13 |             entities: z.array(z.object({
 14 |                 entity: EntitySchema,
 15 |                 parent: EntityIdSchema.optional().describe('The parent entity to create the entity under. If not provided, the root entity will be used.')
 16 |             })).nonempty().describe('Array of entity hierarchies to create.')
 17 |         },
 18 |         ({ entities }) => {
 19 |             return wss.call('entities:create', entities);
 20 |         }
 21 |     );
 22 | 
 23 |     mcp.tool(
 24 |         'modify_entities',
 25 |         'Modify one or more entity\'s properties',
 26 |         {
 27 |             edits: z.array(z.object({
 28 |                 id: EntityIdSchema,
 29 |                 path: z.string().describe('The path to the property to modify. Use dot notation to access nested properties.'),
 30 |                 value: z.any().describe('The value to set the property to.')
 31 |             })).nonempty().describe('An array of objects containing the ID of the entity to modify, the path to the property to modify, and the value to set the property to.')
 32 |         },
 33 |         ({ edits }) => {
 34 |             return wss.call('entities:modify', edits);
 35 |         }
 36 |     );
 37 | 
 38 |     mcp.tool(
 39 |         'duplicate_entities',
 40 |         'Duplicate one or more entities',
 41 |         {
 42 |             ids: z.array(EntityIdSchema).nonempty().describe('Array of entity IDs to duplicate. The root entity cannot be duplicated.'),
 43 |             rename: z.boolean().optional()
 44 |         },
 45 |         ({ ids, rename }) => {
 46 |             return wss.call('entities:duplicate', ids, { rename });
 47 |         }
 48 |     );
 49 | 
 50 |     mcp.tool(
 51 |         'reparent_entity',
 52 |         'Reparent an entity',
 53 |         {
 54 |             id: EntityIdSchema,
 55 |             parent: EntityIdSchema,
 56 |             index: z.number().optional(),
 57 |             preserveTransform: z.boolean().optional()
 58 |         },
 59 |         (options) => {
 60 |             return wss.call('entities:reparent', options);
 61 |         }
 62 |     );
 63 | 
 64 |     mcp.tool(
 65 |         'delete_entities',
 66 |         'Delete one or more entities. The root entity cannot be deleted.',
 67 |         {
 68 |             ids: z.array(EntityIdSchema).nonempty().describe('Array of entity IDs to delete. The root entity cannot be deleted.')
 69 |         },
 70 |         ({ ids }) => {
 71 |             return wss.call('entities:delete', ids);
 72 |         }
 73 |     );
 74 | 
 75 |     mcp.tool(
 76 |         'list_entities',
 77 |         'List all entities',
 78 |         {},
 79 |         () => {
 80 |             return wss.call('entities:list');
 81 |         }
 82 |     );
 83 | 
 84 |     mcp.tool(
 85 |         'add_components',
 86 |         'Add components to an entity',
 87 |         {
 88 |             id: EntityIdSchema,
 89 |             components: ComponentsSchema
 90 |         },
 91 |         ({ id, components }) => {
 92 |             return wss.call('entities:components:add', id, components);
 93 |         }
 94 |     );
 95 | 
 96 |     mcp.tool(
 97 |         'remove_components',
 98 |         'Remove components from an entity',
 99 |         {
100 |             id: EntityIdSchema,
101 |             components: z.array(ComponentNameSchema).nonempty().describe('Array of component names to remove from the entity.')
102 |         },
103 |         ({ id, components }) => {
104 |             return wss.call('entities:components:remove', id, components);
105 |         }
106 |     );
107 | 
108 |     mcp.tool(
109 |         'add_script_component_script',
110 |         'Add a script to a script component',
111 |         {
112 |             id: EntityIdSchema,
113 |             scriptName: z.string()
114 |         },
115 |         ({ id, scriptName }) => {
116 |             return wss.call('entities:components:script:add', id, scriptName);
117 |         }
118 |     );
119 | };
120 | 
```

--------------------------------------------------------------------------------
/extension/popup.js:
--------------------------------------------------------------------------------

```javascript
  1 | const DEFAULT_PORT = 52000;
  2 | 
  3 | // UI
  4 | const root = document.createElement('div');
  5 | root.id = 'root';
  6 | document.body.appendChild(root);
  7 | 
  8 | const header = document.createElement('div');
  9 | header.classList.add('header');
 10 | header.textContent = 'PlayCanvas Editor MCP Extension';
 11 | root.appendChild(header);
 12 | 
 13 | const body = document.createElement('div');
 14 | body.classList.add('body');
 15 | root.appendChild(body);
 16 | 
 17 | const statusGroup = document.createElement('div');
 18 | statusGroup.classList.add('group');
 19 | body.appendChild(statusGroup);
 20 | 
 21 | const statusInd = document.createElement('div');
 22 | statusInd.classList.add('indicator');
 23 | statusGroup.appendChild(statusInd);
 24 | 
 25 | const statusLabel = document.createElement('label');
 26 | statusLabel.classList.add('label');
 27 | statusLabel.textContent = 'Disconnected';
 28 | statusGroup.appendChild(statusLabel);
 29 | 
 30 | const portInput = document.createElement('input');
 31 | portInput.classList.add('input');
 32 | portInput.type = 'text';
 33 | portInput.placeholder = 'Enter port number';
 34 | portInput.value = DEFAULT_PORT;
 35 | body.appendChild(portInput);
 36 | 
 37 | const connectBtn = document.createElement('button');
 38 | connectBtn.classList.add('button');
 39 | connectBtn.textContent = 'CONNECT';
 40 | body.appendChild(connectBtn);
 41 | 
 42 | /**
 43 |  * Creates a state management hook.
 44 |  *
 45 |  * @param {string} defaultState - The default state.
 46 |  * @returns {[function(): string, function(string): void]} The state getter and setter.
 47 |  */
 48 | const useState = (defaultState) => {
 49 |     let state;
 50 |     const get = () => state;
 51 |     const set = (value) => {
 52 |         state = value;
 53 |         switch (state) {
 54 |             case 'disconnected': {
 55 |                 statusInd.classList.remove('connecting', 'connected');
 56 |                 statusInd.classList.add('disconnected');
 57 |                 statusLabel.textContent = 'Disconnected';
 58 | 
 59 |                 portInput.disabled = false;
 60 |                 portInput.classList.remove('disabled');
 61 | 
 62 |                 connectBtn.textContent = 'CONNECT';
 63 | 
 64 |                 break;
 65 |             }
 66 |             case 'connecting': {
 67 |                 statusInd.classList.remove('connected', 'disconnected');
 68 |                 statusInd.classList.add('connecting');
 69 |                 statusLabel.textContent = 'Connecting';
 70 | 
 71 |                 portInput.disabled = true;
 72 |                 portInput.classList.add('disabled');
 73 | 
 74 |                 connectBtn.textContent = 'CANCEL';
 75 |                 break;
 76 |             }
 77 |             case 'connected': {
 78 |                 statusInd.classList.remove('disconnected', 'connecting');
 79 |                 statusInd.classList.add('connected');
 80 |                 statusLabel.textContent = 'Connected';
 81 | 
 82 |                 portInput.disabled = true;
 83 |                 portInput.classList.add('disabled');
 84 | 
 85 |                 connectBtn.textContent = 'DISCONNECT';
 86 |                 break;
 87 |             }
 88 |         }
 89 |     };
 90 |     set(defaultState);
 91 |     return [get, set];
 92 | };
 93 | const [getState, setState] = useState('disconnected');
 94 | 
 95 | /**
 96 |  * Event handler
 97 |  */
 98 | class EventHandler {
 99 |     _handlers = new Map();
100 | 
101 |     /**
102 |      * @param {string} name - The name of the event to add.
103 |      * @param {(...args: any[]) => void} fn - The function to call when the event is triggered.
104 |      */
105 |     on(name, fn) {
106 |         if (!this._handlers.has(name)) {
107 |             this._handlers.set(name, []);
108 |         }
109 |         this._handlers.get(name).push(fn);
110 |     }
111 | 
112 |     /**
113 |      * @param {string} name - The name of the event to remove.
114 |      * @param {(...args: any[]) => void} fn - The function to remove.
115 |      */
116 |     off(name, fn) {
117 |         if (!this._handlers.has(name)) {
118 |             return;
119 |         }
120 |         const methods = this._handlers.get(name);
121 |         const index = methods.indexOf(fn);
122 |         if (index !== -1) {
123 |             methods.splice(index, 1);
124 |         }
125 |         if (methods.length === 0) {
126 |             this._handlers.delete(name);
127 |         }
128 |     }
129 | 
130 |     /**
131 |      * @param {string} name - The name of the event to trigger.
132 |      * @param {...*} args - The arguments to pass to the event.
133 |      */
134 |     fire(name, ...args) {
135 |         if (!this._handlers.has(name)) {
136 |             return;
137 |         }
138 |         const handlers = this._handlers.get(name);
139 |         for (let i = 0; i < handlers.length; i++) {
140 |             handlers[i](...args);
141 |         }
142 |     }
143 | }
144 | 
145 | // Listen for messages from the content script
146 | const listener = new EventHandler();
147 | listener.on('status', (status) => {
148 |     setState(status);
149 | });
150 | chrome.runtime.onMessage.addListener((data) => {
151 |     const { name, args } = data;
152 |     listener.fire(name, ...args);
153 | });
154 | 
155 | /**
156 |  * Sends a message to the content script.
157 |  *
158 |  * @param {string} name - The name of the message to send.
159 |  * @param {...*} args - The arguments to pass to the message.
160 |  * @returns {Promise<boolean>} - A promise that resolves to true if the message was sent successfully, false otherwise.
161 |  */
162 | const send = async (name, ...args) => {
163 |     const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
164 |     if (!tab) {
165 |         return false;
166 |     }
167 |     if (!/playcanvas\.com\/editor/.test(tab.url)) {
168 |         return false;
169 |     }
170 |     chrome.tabs.sendMessage(tab.id, { name, args });
171 |     return true;
172 | };
173 | 
174 | connectBtn.addEventListener('click', () => {
175 |     if (getState() === 'disconnected') {
176 |         setState('connecting');
177 |         send('connect', {
178 |             port: portInput.value
179 |         }).catch((e) => {
180 |             console.error('SEND ERROR:', e);
181 |         });
182 |     } else {
183 |         send('disconnect').catch((e) => {
184 |             console.error('SEND ERROR:', e);
185 |         });
186 |     }
187 | });
188 | 
189 | send('sync').then((success) => {
190 |     if (!success) {
191 |         portInput.disabled = true;
192 |         portInput.classList.add('disabled');
193 | 
194 |         connectBtn.disabled = true;
195 |         connectBtn.classList.add('disabled');
196 |     }
197 | }).catch((e) => {
198 |     console.error('SEND ERROR:', e);
199 | });
200 | 
```

--------------------------------------------------------------------------------
/src/tools/schema/scene-settings.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | 
 3 | import { AssetIdSchema, RgbSchema, Vec3Schema } from './common';
 4 | 
 5 | const PhysicsSchema = z.object({
 6 |     gravity: Vec3Schema.optional().describe('An array of 3 numbers that represents the gravity force. Default: [0, -9.8, 0]')
 7 | }).describe('Physics related settings for the scene.');
 8 | 
 9 | const RenderSchema = z.object({
10 |     fog: z.enum(['none', 'linear', 'exp', 'exp2']).optional().describe('The type of fog used in the scene. Can be one of `pc.FOG_NONE`, `pc.FOG_LINEAR`, `pc.FOG_EXP`, `pc.FOG_EXP2`. Default: `none`.'),
11 |     fog_start: z.number().min(0).optional().describe('The distance from the viewpoint where linear fog begins. This property is only valid if the fog property is set to `pc.FOG_LINEAR`. Default: 1.0.'),
12 |     fog_end: z.number().min(0).optional().describe('The distance from the viewpoint where linear fog reaches its maximum. This property is only valid if the fog property is set to `pc.FOG_LINEAR`. Default: 1000.0.'),
13 |     fog_density: z.number().min(0).optional().describe('The density of the fog. This property is only valid if the fog property is set to `pc.FOG_EXP` or `pc.FOG_EXP2`. Default: 0.01.'),
14 |     fog_color: RgbSchema.optional().describe('An array of 3 numbers representing the color of the fog. Default: [0.0, 0.0, 0.0].'),
15 |     global_ambient: RgbSchema.optional().describe('An array of 3 numbers representing the color of the scene\'s ambient light. Default: [0.2, 0.2, 0.2].'),
16 |     gamma_correction: z.union([
17 |         z.literal(0).describe('GAMMA_NONE'),
18 |         z.literal(1).describe('GAMMA_SRGB')
19 |     ]).optional().describe('The gamma correction to apply when rendering the scene. Default: 1 (GAMMA_SRGB).'),
20 |     lightmapSizeMultiplier: z.number().optional().describe('The lightmap resolution multiplier. Default: 16.'),
21 |     lightmapMaxResolution: z.number().optional().describe('The maximum lightmap resolution. Default: 2048.'),
22 |     lightmapMode: z.union([
23 |         z.literal(0).describe('BAKE_COLOR'),
24 |         z.literal(1).describe('BAKE_COLORDIR')
25 |     ]).optional().describe('The lightmap baking mode. Default: 1 (BAKE_COLORDIR).'),
26 |     tonemapping: z.number().optional().describe('The tonemapping transform to apply when writing fragments to the frame buffer. Default: 0.'),
27 |     exposure: z.number().optional().describe('The exposure value tweaks the overall brightness of the scene. Default: 1.0.'),
28 |     skybox: AssetIdSchema.optional().describe('The `id` of the cubemap texture to be used as the scene\'s skybox. Default: null.'),
29 |     skyType: z.enum(['infinite', 'box', 'dome']).optional().describe('Type of skybox projection. Default: `infinite`.'),
30 |     skyMeshPosition: Vec3Schema.optional().describe('An array of 3 numbers representing the position of the sky mesh. Default: [0.0, 0.0, 0.0].'),
31 |     skyMeshRotation: Vec3Schema.optional().describe('An array of 3 numbers representing the rotation of the sky mesh. Default: [0.0, 0.0, 0.0].'),
32 |     skyMeshScale: Vec3Schema.optional().describe('An array of 3 numbers representing the scale of the sky mesh. Default: [100.0, 100.0, 100.0].'),
33 |     skyCenter: Vec3Schema.optional().describe('An array of 3 numbers representing the center of the sky mesh. Default: [0.0, 0.1, 0.0].'),
34 |     skyboxIntensity: z.number().optional().describe('Multiplier for skybox intensity. Default: 1.'),
35 |     skyboxMip: z.number().int().min(0).max(5).optional().describe('The mip level of the skybox to be displayed. Only valid for prefiltered cubemap skyboxes. Default: 0.'),
36 |     skyboxRotation: Vec3Schema.optional().describe('An array of 3 numbers representing the rotation of the skybox. Default: [0, 0, 0].'),
37 |     lightmapFilterEnabled: z.boolean().optional().describe('Enable filtering of lightmaps. Default: false.'),
38 |     lightmapFilterRange: z.number().optional().describe('A range parameter of the bilateral filter. Default: 10.'),
39 |     lightmapFilterSmoothness: z.number().optional().describe('A spatial parameter of the bilateral filter. Default: 0.2.'),
40 |     ambientBake: z.boolean().optional().describe('Enable baking the ambient lighting into lightmaps. Default: false.'),
41 |     ambientBakeNumSamples: z.number().optional().describe('Number of samples to use when baking ambient. Default: 1.'),
42 |     ambientBakeSpherePart: z.number().optional().describe('How much of the sphere to include when baking ambient. Default: 0.4.'),
43 |     ambientBakeOcclusionBrightness: z.number().optional().describe('Specifies the ambient occlusion brightness. Typical range is -1 to 1. Default: 0.'),
44 |     ambientBakeOcclusionContrast: z.number().optional().describe('Specifies the ambient occlusion contrast. Typical range is -1 to 1. Default: 0.'),
45 |     clusteredLightingEnabled: z.boolean().optional().describe('Enable the clustered lighting. Default: true.'),
46 |     lightingCells: Vec3Schema.optional().describe('Number of cells along each world-space axis the space containing lights is subdivided into. Default: [10, 3, 10].'),
47 |     lightingMaxLightsPerCell: z.number().optional().describe('Maximum number of lights a cell can store. Default: 255.'),
48 |     lightingCookieAtlasResolution: z.number().optional().describe('Resolution of the atlas texture storing all non-directional cookie textures. Default: 2048.'),
49 |     lightingShadowAtlasResolution: z.number().optional().describe('Resolution of the atlas texture storing all non-directional shadow textures. Default: 2048.'),
50 |     lightingShadowType: z.union([
51 |         z.literal(0).describe('SHADOW_PCF3_32F'),
52 |         z.literal(4).describe('SHADOW_PCF5_32F'),
53 |         z.literal(5).describe('SHADOW_PCF1_32F')
54 |     ]).optional().describe('The type of shadow filtering used by all shadows. Default: 0 (SHADOW_PCF3_32F).'),
55 |     lightingCookiesEnabled: z.boolean().optional().describe('Cluster lights support cookies. Default: false.'),
56 |     lightingAreaLightsEnabled: z.boolean().optional().describe('Cluster lights support area lights. Default: false.'),
57 |     lightingShadowsEnabled: z.boolean().optional().describe('Cluster lights support shadows. Default: true.')
58 | }).describe('Render related settings for the scene.');
59 | 
60 | const SceneSettingsSchema = z.object({
61 |     physics: PhysicsSchema.optional(),
62 |     render: RenderSchema.optional()
63 | }).describe('Scene settings.');
64 | 
65 | export { SceneSettingsSchema };
66 | 
```

--------------------------------------------------------------------------------
/src/tools/schema/asset.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod';
  2 | 
  3 | import { AssetIdSchema, EntityIdSchema, RgbSchema, Vec2Schema, Vec3Schema } from './common';
  4 | 
  5 | const MaterialSchema = z.object({
  6 |     name: z.string().optional(),
  7 |     ambient: RgbSchema.optional(),
  8 |     aoMap: AssetIdSchema.optional(),
  9 |     aoMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 10 |     aoMapUv: z.number().int().min(0).max(7).optional(),
 11 |     aoMapTiling: Vec2Schema.optional(),
 12 |     aoMapOffset: Vec2Schema.optional(),
 13 |     aoMapRotation: z.number().optional(),
 14 |     aoVertexColor: z.boolean().optional(),
 15 |     aoVertexColorChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 16 |     aoIntensity: z.number().optional(),
 17 |     diffuse: RgbSchema.optional(),
 18 |     diffuseMap: AssetIdSchema.optional(),
 19 |     diffuseMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 20 |     diffuseMapUv: z.number().int().min(0).max(7).optional(),
 21 |     diffuseMapTiling: Vec2Schema.optional(),
 22 |     diffuseMapOffset: Vec2Schema.optional(),
 23 |     diffuseMapRotation: z.number().optional(),
 24 |     diffuseVertexColor: z.boolean().optional(),
 25 |     diffuseVertexColorChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 26 |     specular: RgbSchema.optional(),
 27 |     specularMap: AssetIdSchema.optional(),
 28 |     specularMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 29 |     specularMapUv: z.number().int().min(0).max(7).optional(),
 30 |     specularMapTiling: Vec2Schema.optional(),
 31 |     specularMapOffset: Vec2Schema.optional(),
 32 |     specularMapRotation: z.number().optional(),
 33 |     specularAntialias: z.boolean().optional(),
 34 |     specularTint: z.boolean().optional(),
 35 |     specularVertexColor: z.boolean().optional(),
 36 |     specularVertexColorChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 37 |     occludeSpecular: z.number().int().min(0).max(2).optional(),
 38 |     specularityFactor: z.number().optional(),
 39 |     specularityFactorMap: AssetIdSchema.optional(),
 40 |     specularityFactorMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 41 |     specularityFactorMapUv: z.number().int().min(0).max(7).optional(),
 42 |     specularityFactorMapTiling: Vec2Schema.optional(),
 43 |     specularityFactorMapOffset: Vec2Schema.optional(),
 44 |     specularityFactorMapRotation: z.number().optional(),
 45 |     specularityFactorTint: z.boolean().optional(),
 46 |     specularityFactorVertexColor: z.boolean().optional(),
 47 |     specularityFactorVertexColorChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 48 |     enableGGXSpecular: z.boolean().optional(),
 49 |     anisotropy: z.number().min(-1).max(1).optional(),
 50 |     useMetalness: z.boolean().optional(),
 51 |     metalness: z.number().min(0).max(1).optional(),
 52 |     metalnessMap: AssetIdSchema.optional(),
 53 |     metalnessMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 54 |     metalnessMapUv: z.number().int().min(0).max(7).optional(),
 55 |     metalnessMapTiling: Vec2Schema.optional(),
 56 |     metalnessMapOffset: Vec2Schema.optional(),
 57 |     metalnessMapRotation: z.number().optional(),
 58 |     metalnessVertexColor: z.boolean().optional(),
 59 |     metalnessVertexColorChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 60 |     useMetalnessSpecularColor: z.boolean().optional(),
 61 |     conserveEnergy: z.boolean().optional(),
 62 |     shininess: z.number().min(0).max(100).optional(),
 63 |     glossMap: AssetIdSchema.optional(),
 64 |     glossMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 65 |     glossMapUv: z.number().int().min(0).max(7).optional(),
 66 |     glossMapTiling: Vec2Schema.optional(),
 67 |     glossMapOffset: Vec2Schema.optional(),
 68 |     glossMapRotation: z.number().optional(),
 69 |     glossVertexColor: z.boolean().optional(),
 70 |     glossVertexColorChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 71 |     glossInvert: z.boolean().optional(),
 72 |     clearCoat: z.number().min(0).max(1).optional(),
 73 |     clearCoatMap: AssetIdSchema.optional(),
 74 |     clearCoatMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 75 |     clearCoatMapUv: z.number().int().min(0).max(7).optional(),
 76 |     clearCoatMapTiling: Vec2Schema.optional(),
 77 |     clearCoatMapOffset: Vec2Schema.optional(),
 78 |     clearCoatMapRotation: z.number().optional(),
 79 |     clearCoatVertexColor: z.boolean().optional(),
 80 |     clearCoatVertexColorChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 81 |     clearCoatGloss: z.number().min(0).max(1).optional(),
 82 |     clearCoatGlossMap: AssetIdSchema.optional(),
 83 |     clearCoatGlossMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 84 |     clearCoatGlossMapUv: z.number().int().min(0).max(7).optional(),
 85 |     clearCoatGlossMapTiling: Vec2Schema.optional(),
 86 |     clearCoatGlossMapOffset: Vec2Schema.optional(),
 87 |     clearCoatGlossMapRotation: z.number().optional(),
 88 |     clearCoatGlossVertexColor: z.boolean().optional(),
 89 |     clearCoatGlossVertexColorChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
 90 |     clearCoatGlossInvert: z.boolean().optional(),
 91 |     clearCoatNormalMap: AssetIdSchema.optional(),
 92 |     clearCoatNormalMapUv: z.number().int().min(0).max(7).optional(),
 93 |     clearCoatNormalMapTiling: Vec2Schema.optional(),
 94 |     clearCoatNormalMapOffset: Vec2Schema.optional(),
 95 |     clearCoatNormalMapRotation: z.number().optional(),
 96 |     useSheen: z.boolean().optional(),
 97 |     sheen: RgbSchema.optional(),
 98 |     sheenMap: AssetIdSchema.optional(),
 99 |     sheenMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
100 |     sheenMapUv: z.number().int().min(0).max(7).optional(),
101 |     sheenMapTiling: Vec2Schema.optional(),
102 |     sheenMapOffset: Vec2Schema.optional(),
103 |     sheenMapRotation: z.number().optional(),
104 |     sheenVertexColor: z.boolean().optional(),
105 |     sheenVertexColorChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
106 |     sheenGloss: z.number().optional(),
107 |     sheenGlossMap: AssetIdSchema.optional(),
108 |     sheenGlossMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
109 |     sheenGlossMapUv: z.number().int().min(0).max(7).optional(),
110 |     sheenGlossMapTiling: Vec2Schema.optional(),
111 |     sheenGlossMapOffset: Vec2Schema.optional(),
112 |     sheenGlossMapRotation: z.number().optional(),
113 |     sheenGlossVertexColor: z.boolean().optional(),
114 |     sheenGlossVertexColorChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
115 |     sheenGlossInvert: z.boolean().optional(),
116 |     emissive: RgbSchema.optional(),
117 |     emissiveMap: AssetIdSchema.optional(),
118 |     emissiveMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
119 |     emissiveMapUv: z.number().int().min(0).max(7).optional(),
120 |     emissiveMapTiling: Vec2Schema.optional(),
121 |     emissiveMapOffset: Vec2Schema.optional(),
122 |     emissiveMapRotation: z.number().optional(),
123 |     emissiveIntensity: z.number().min(0).max(10).optional(),
124 |     emissiveVertexColor: z.boolean().optional(),
125 |     emissiveVertexColorChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
126 |     normalMap: AssetIdSchema.optional(),
127 |     normalMapUv: z.number().int().min(0).max(7).optional(),
128 |     normalMapTiling: Vec2Schema.optional(),
129 |     normalMapOffset: Vec2Schema.optional(),
130 |     normalMapRotation: z.number().optional(),
131 |     bumpMapFactor: z.number().optional(),
132 |     useDynamicRefraction: z.boolean().optional(),
133 |     refraction: z.number().min(0).max(1).optional(),
134 |     refractionMap: AssetIdSchema.optional(),
135 |     refractionMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
136 |     refractionMapUv: z.number().int().min(0).max(7).optional(),
137 |     refractionMapTiling: Vec2Schema.optional(),
138 |     refractionMapOffset: Vec2Schema.optional(),
139 |     refractionMapRotation: z.number().optional(),
140 |     refractionVertexColor: z.boolean().optional(),
141 |     refractionVertexColorChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
142 |     refractionIndex: z.number().min(0).max(1).optional(),
143 |     dispersion: z.number().min(0).max(10).optional(),
144 |     thickness: z.number().optional(),
145 |     thicknessMap: AssetIdSchema.optional(),
146 |     thicknessMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
147 |     thicknessMapUv: z.number().int().min(0).max(7).optional(),
148 |     thicknessMapTiling: Vec2Schema.optional(),
149 |     thicknessMapOffset: Vec2Schema.optional(),
150 |     thicknessMapRotation: z.number().optional(),
151 |     thicknessVertexColor: z.boolean().optional(),
152 |     thicknessVertexColorChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
153 |     attenuation: z.array(z.number()).length(3).optional(),
154 |     attenuationDistance: z.number().optional(),
155 |     useIridescence: z.boolean().optional(),
156 |     iridescence: z.number().optional(),
157 |     iridescenceMap: AssetIdSchema.optional(),
158 |     iridescenceMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
159 |     iridescenceMapUv: z.number().int().min(0).max(7).optional(),
160 |     iridescenceMapTiling: Vec2Schema.optional(),
161 |     iridescenceMapOffset: Vec2Schema.optional(),
162 |     iridescenceMapRotation: z.number().optional(),
163 |     iridescenceRefractionIndex: z.number().optional(),
164 |     iridescenceThicknessMap: AssetIdSchema.optional(),
165 |     iridescenceThicknessMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
166 |     iridescenceThicknessMapUv: z.number().int().min(0).max(7).optional(),
167 |     iridescenceThicknessMapTiling: Vec2Schema.optional(),
168 |     iridescenceThicknessMapOffset: Vec2Schema.optional(),
169 |     iridescenceThicknessMapRotation: z.number().optional(),
170 |     iridescenceThicknessMin: z.number().optional(),
171 |     iridescenceThicknessMax: z.number().optional(),
172 |     heightMap: AssetIdSchema.optional(),
173 |     heightMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
174 |     heightMapUv: z.number().int().min(0).max(7).optional(),
175 |     heightMapTiling: Vec2Schema.optional(),
176 |     heightMapOffset: Vec2Schema.optional(),
177 |     heightMapRotation: z.number().optional(),
178 |     heightMapFactor: z.number().min(0).max(2).optional(),
179 |     alphaToCoverage: z.boolean().optional(),
180 |     alphaTest: z.number().min(0).max(1).optional(),
181 |     alphaFade: z.number().min(0).max(1).optional(),
182 |     opacity: z.number().min(0).max(1).optional(),
183 |     opacityMap: AssetIdSchema.optional(),
184 |     opacityMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
185 |     opacityMapUv: z.number().int().min(0).max(7).optional(),
186 |     opacityMapTiling: Vec2Schema.optional(),
187 |     opacityMapOffset: Vec2Schema.optional(),
188 |     opacityMapRotation: z.number().optional(),
189 |     opacityVertexColor: z.boolean().optional(),
190 |     opacityVertexColorChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
191 |     opacityFadesSpecular: z.boolean().optional(),
192 |     opacityDither: z.enum(['none', 'bayer8', 'bluenoise', 'ignnoise']).optional(),
193 |     opacityShadowDither: z.enum(['none', 'bayer8', 'bluenoise', 'ignnoise']).optional(),
194 |     reflectivity: z.number().min(0).max(1).optional(),
195 |     sphereMap: AssetIdSchema.optional(),
196 |     cubeMap: AssetIdSchema.optional(),
197 |     cubeMapProjection: z.number().int().min(0).max(1).optional(),
198 |     cubeMapProjectionBox: z.object({
199 |         center: Vec3Schema,
200 |         halfExtents: Vec3Schema
201 |     }).optional(),
202 |     lightMap: AssetIdSchema.optional(),
203 |     lightMapChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
204 |     lightMapUv: z.number().int().min(0).max(7).optional(),
205 |     lightMapTiling: Vec2Schema.optional(),
206 |     lightMapOffset: Vec2Schema.optional(),
207 |     lightMapRotation: z.number().optional(),
208 |     lightVertexColor: z.boolean().optional(),
209 |     lightVertexColorChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional(),
210 |     depthTest: z.boolean().optional(),
211 |     depthWrite: z.boolean().optional(),
212 |     depthBias: z.number().optional(),
213 |     slopeDepthBias: z.number().optional(),
214 |     cull: z.number().int().min(0).max(3).optional(),
215 |     blendType: z.number().int().min(0).max(10).optional(),
216 |     useFog: z.boolean().optional(),
217 |     useLighting: z.boolean().optional(),
218 |     useSkybox: z.boolean().optional(),
219 |     useTonemap: z.boolean().optional(),
220 |     twoSidedLighting: z.boolean().optional()
221 | });
222 | 
223 | export const CssCreateSchema = z.object({
224 |     type: z.literal('css'),
225 |     options: z.object({
226 |         folder: AssetIdSchema.optional(),
227 |         name: z.string().optional(),
228 |         preload: z.boolean().optional(),
229 |         text: z.string().optional()
230 |     }).optional()
231 | }).describe('CSS asset creation options.');
232 | 
233 | export const HtmlCreateSchema = z.object({
234 |     type: z.literal('html'),
235 |     options: z.object({
236 |         folder: AssetIdSchema.optional(),
237 |         name: z.string().optional(),
238 |         preload: z.boolean().optional(),
239 |         text: z.string().optional()
240 |     }).optional()
241 | }).describe('HTML asset creation options.');
242 | 
243 | export const FolderCreateSchema = z.object({
244 |     type: z.literal('folder'),
245 |     options: z.object({
246 |         folder: AssetIdSchema.optional(),
247 |         name: z.string().optional()
248 |     }).optional()
249 | }).describe('Folder asset creation options.');
250 | 
251 | export const MaterialCreateSchema = z.object({
252 |     type: z.literal('material'),
253 |     options: z.object({
254 |         data: MaterialSchema.optional(),
255 |         folder: AssetIdSchema.optional(),
256 |         name: z.string().optional(),
257 |         preload: z.boolean().optional()
258 |     }).optional()
259 | }).describe('Material asset creation options.');
260 | 
261 | export const ScriptCreateSchema = z.object({
262 |     type: z.literal('script'),
263 |     options: z.object({
264 |         filename: z.string().optional(),
265 |         folder: AssetIdSchema.optional(),
266 |         preload: z.boolean().optional(),
267 |         text: z.string().optional()
268 |     }).optional()
269 | }).describe('Script asset creation options.');
270 | 
271 | export const ShaderCreateSchema = z.object({
272 |     type: z.literal('shader'),
273 |     options: z.object({
274 |         folder: AssetIdSchema.optional(),
275 |         name: z.string().optional(),
276 |         preload: z.boolean().optional(),
277 |         text: z.string().optional()
278 |     }).optional()
279 | }).describe('Shader asset creation options.');
280 | 
281 | export const TemplateCreateSchema = z.object({
282 |     type: z.literal('template'),
283 |     options: z.object({
284 |         entity: EntityIdSchema,
285 |         folder: AssetIdSchema.optional(),
286 |         name: z.string().optional(),
287 |         preload: z.boolean().optional()
288 |     })
289 | }).describe('Template asset creation options.');
290 | 
291 | export const TextCreateSchema = z.object({
292 |     type: z.literal('text'),
293 |     options: z.object({
294 |         folder: AssetIdSchema.optional(),
295 |         name: z.string().optional(),
296 |         preload: z.boolean().optional(),
297 |         text: z.string().optional()
298 |     }).optional()
299 | }).describe('Text asset creation options.');
300 | 
```

--------------------------------------------------------------------------------
/extension/main.js:
--------------------------------------------------------------------------------

```javascript
  1 | (() => {
  2 |     if (!window.editor) {
  3 |         throw new Error('PlayCanvas Editor not found');
  4 |     }
  5 | 
  6 |     /**
  7 |      * @param {string} msg - The message to log.
  8 |      */
  9 |     const log = (msg) => {
 10 |         console.log(`%c[WSC] ${msg}`, 'color:#f60');
 11 |     };
 12 | 
 13 |     /**
 14 |      * @param {string} msg - The message to log.
 15 |      */
 16 |     const error = (msg) => {
 17 |         console.error(`%c[WSC] ${msg}`, 'color:#f60');
 18 |     };
 19 | 
 20 |     /**
 21 |      * PlayCanvas Editor API observer package.
 22 |      */
 23 |     const observer = window.editor.observer;
 24 | 
 25 |     /**
 26 |      * PlayCanvas Editor API wrapper.
 27 |      */
 28 |     const api = window.editor.api.globals;
 29 | 
 30 |     /**
 31 |      * PlayCanvas REST API wrapper.
 32 |      *
 33 |      * @param {'GET' | 'POST' | 'PUT' | 'DELETE'} method - The HTTP method to use.
 34 |      * @param {string} path - The path to the API endpoint.
 35 |      * @param {FormData | Object} data - The data to send.
 36 |      * @param {boolean} auth - Whether to use authentication.
 37 |      * @returns {Promise<Object>} The response data.
 38 |      */
 39 |     const rest = (method, path, data, auth = false) => {
 40 |         const init = {
 41 |             method,
 42 |             headers: {
 43 |                 Authorization: auth ? `Bearer ${api.accessToken}` : undefined
 44 |             }
 45 |         };
 46 |         if (data instanceof FormData) {
 47 |             init.body = data;
 48 |         } else {
 49 |             init.headers['Content-Type'] = 'application/json';
 50 |             init.body = JSON.stringify(data);
 51 |         }
 52 |         return fetch(`/api/${path}`, init).then((res) => res.json());
 53 |     };
 54 | 
 55 |     /**
 56 |      * @param {Object} obj - The object to iterate.
 57 |      * @param {Function} callback - The callback to call for each key-value pair.
 58 |      * @param {string} currentPath - The current path of the object.
 59 |      */
 60 |     const iterateObject = (obj, callback, currentPath = '') => {
 61 |         Object.entries(obj).forEach(([key, value]) => {
 62 |             const path = currentPath ? `${currentPath}.${key}` : key;
 63 |             if (value && typeof value === 'object' && !Array.isArray(value)) {
 64 |                 iterateObject(value, callback, path);
 65 |             } else {
 66 |                 callback(path, value);
 67 |             }
 68 |         });
 69 |     };
 70 | 
 71 |     class WSC extends observer.Events {
 72 |         static STATUS_CONNECTING = 'connecting';
 73 | 
 74 |         static STATUS_CONNECTED = 'connected';
 75 | 
 76 |         static STATUS_DISCONNECTED = 'disconnected';
 77 | 
 78 |         /**
 79 |          * @type {WebSocket}
 80 |          * @private
 81 |          */
 82 |         _ws;
 83 | 
 84 |         /**
 85 |          * @type {Map<string, Function}
 86 |          * @private
 87 |          */
 88 |         _methods = new Map();
 89 | 
 90 |         /**
 91 |          * @type {ReturnType<typeof setTimeout> |  null}
 92 |          * @private
 93 |          */
 94 |         _connectTimeout = null;
 95 | 
 96 |         /**
 97 |          * @type {WSC.STATUS_CONNECTING | WSC.STATUS_CONNECTED | WSC.STATUS_DISCONNECTED}
 98 |          * @private
 99 |          */
100 |         _status = WSC.STATUS_DISCONNECTED;
101 | 
102 |         /**
103 |          * @type {WSC.STATUS_CONNECTING | WSC.STATUS_CONNECTED | WSC.STATUS_DISCONNECTED}
104 |          */
105 |         get status() {
106 |             return this._status;
107 |         }
108 | 
109 |         /**
110 |          * @param {string} address - The address to connect to.
111 |          * @param {Function} resolve - The function to call when the connection is established.
112 |          * @param {number} [retryTimeout] - The timeout to retry the connection.
113 |          */
114 |         connect(address, retryTimeout = 1000) {
115 |             this._status = WSC.STATUS_CONNECTING;
116 |             this.emit('status', this._status);
117 |             log(`Connecting to ${address}`);
118 | 
119 |             if (this._connectTimeout) {
120 |                 clearTimeout(this._connectTimeout);
121 |             }
122 | 
123 |             this._connect(address, retryTimeout, () => {
124 |                 this._ws.onclose = (evt) => {
125 |                     if (evt.reason === 'FORCE') {
126 |                         return;
127 |                     }
128 |                     this._status = WSC.STATUS_DISCONNECTED;
129 |                     this.emit('status', this._status);
130 |                     log('Disconnected');
131 |                 };
132 | 
133 |                 this._status = WSC.STATUS_CONNECTED;
134 |                 this.emit('status', this._status);
135 |                 log('Connected');
136 |             });
137 |         }
138 | 
139 |         /**
140 |          * @param {string} address - The address to connect to.
141 |          * @param {number} retryTimeout - The timeout to retry the connection.
142 |          * @param {Function} resolve - The function to call when the connection is established
143 |          * @private
144 |          */
145 |         _connect(address, retryTimeout, resolve) {
146 |             this._ws = new WebSocket(address);
147 |             this._ws.onopen = () => {
148 |                 resolve();
149 |             };
150 |             this._ws.onmessage = async (event) => {
151 |                 try {
152 |                     const { id, name, args } = JSON.parse(event.data);
153 |                     const res = await this.call(name, ...args);
154 |                     this._ws.send(JSON.stringify({ id, res }));
155 |                 } catch (e) {
156 |                     error(e);
157 |                 }
158 |             };
159 |             this._ws.onclose = () => {
160 |                 this._connectTimeout = setTimeout(() => {
161 |                     this._connectTimeout = null;
162 |                     this._connect(address, retryTimeout, resolve);
163 |                 }, retryTimeout);
164 |             };
165 |         }
166 | 
167 |         disconnect() {
168 |             if (this._connectTimeout) {
169 |                 clearTimeout(this._connectTimeout);
170 |             }
171 |             if (this._ws) {
172 |                 this._ws.close(1000, 'FORCE');
173 |                 this._ws = null;
174 |             }
175 |             this._status = WSC.STATUS_DISCONNECTED;
176 |             this.emit('status', this._status);
177 |             log('Disconnected');
178 |         }
179 | 
180 |         /**
181 |          * @param {string} name - The name of the method to add.
182 |          * @param {(...args: any[]) => { data?: any, error?: string }} fn - The function to call when the method is called.
183 |          */
184 |         method(name, fn) {
185 |             if (this._methods.get(name)) {
186 |                 error(`Method already exists: ${name}`);
187 |                 return;
188 |             }
189 |             this._methods.set(name, fn);
190 |         }
191 | 
192 |         /**
193 |          * @param {string} name - The name of the method to call.
194 |          * @param {...*} args - The arguments to pass to the method.
195 |          * @returns {{ data?: any, error?: string }} The response data.
196 |          */
197 |         call(name, ...args) {
198 |             return this._methods.get(name)?.(...args);
199 |         }
200 |     }
201 | 
202 |     class Messenger extends observer.Events {
203 |         constructor() {
204 |             super();
205 | 
206 |             window.addEventListener('message', (event) => {
207 |                 if (event.data?.ctx !== 'isolated') {
208 |                     return;
209 |                 }
210 |                 const { name, args } = event.data;
211 |                 this.emit(name, ...args);
212 |             });
213 |         }
214 | 
215 |         send(name, ...args) {
216 |             window.postMessage({ name, args, ctx: 'main' });
217 |         }
218 |     }
219 | 
220 |     const wsc = new WSC();
221 |     const messenger = new Messenger('main');
222 | 
223 |     // sync
224 |     messenger.on('sync', () => {
225 |         messenger.send('status', wsc.status);
226 |     });
227 |     messenger.on('connect', ({ port = 52000 }) => {
228 |         wsc.connect(`ws://localhost:${port}`);
229 |     });
230 |     messenger.on('disconnect', () => {
231 |         wsc.disconnect();
232 |     });
233 |     wsc.on('status', (status) => {
234 |         messenger.send('status', status);
235 |     });
236 | 
237 |     // general
238 |     wsc.method('ping', () => 'pong');
239 | 
240 |     // entities
241 |     wsc.method('entities:create', (entityDataArray) => {
242 |         const entities = [];
243 |         entityDataArray.forEach((entityData) => {
244 |             if (Object.hasOwn(entityData, 'parent')) {
245 |                 const parent = api.entities.get(entityData.parent);
246 |                 if (!parent) {
247 |                     return { error: `Parent entity not found: ${entityData.parent}` };
248 |                 }
249 |                 entityData.entity.parent = parent;
250 |             }
251 | 
252 |             const entity = api.entities.create(entityData.entity);
253 |             if (!entity) {
254 |                 return { error: 'Failed to create entity' };
255 |             }
256 |             entities.push(entity);
257 | 
258 |             log(`Created entity(${entity.get('resource_id')})`);
259 |         });
260 |         return { data: entities.map((entity) => entity.json()) };
261 |     });
262 |     wsc.method('entities:modify', (edits) => {
263 |         edits.forEach(({ id, path, value }) => {
264 |             const entity = api.entities.get(id);
265 |             if (!entity) {
266 |                 return { error: 'Entity not found' };
267 |             }
268 |             entity.set(path, value);
269 |             log(`Set property(${path}) of entity(${id}) to: ${JSON.stringify(value)}`);
270 |         });
271 |         return { data: true };
272 |     });
273 |     wsc.method('entities:duplicate', async (ids, options = {}) => {
274 |         const entities = ids.map((id) => api.entities.get(id));
275 |         if (!entities.length) {
276 |             return { error: 'Entities not found' };
277 |         }
278 |         const res = await api.entities.duplicate(entities, options);
279 |         log(`Duplicated entities: ${res.map((entity) => entity.get('resource_id')).join(', ')}`);
280 |         return { data: res.map((entity) => entity.json()) };
281 |     });
282 |     wsc.method('entities:reparent', (options) => {
283 |         const entity = api.entities.get(options.id);
284 |         if (!entity) {
285 |             return { error: 'Entity not found' };
286 |         }
287 |         const parent = api.entities.get(options.parent);
288 |         if (!parent) {
289 |             return { error: 'Parent entity not found' };
290 |         }
291 |         entity.reparent(parent, options.index, {
292 |             preserveTransform: options.preserveTransform
293 |         });
294 |         log(`Reparented entity(${options.id}) to entity(${options.parent})`);
295 |         return { data: entity.json() };
296 |     });
297 |     wsc.method('entities:delete', async (ids) => {
298 |         const entities = ids.map((id) => api.entities.get(id)).filter((entity) => entity !== api.entities.root);
299 |         if (!entities.length) {
300 |             return { error: 'No entities to delete' };
301 |         }
302 |         await api.entities.delete(entities);
303 |         log(`Deleted entities: ${ids.join(', ')}`);
304 |         return { data: true };
305 |     });
306 |     wsc.method('entities:list', () => {
307 |         const entities = api.entities.list();
308 |         if (!entities.length) {
309 |             return { error: 'No entities found' };
310 |         }
311 |         log('Listed entities');
312 |         return { data: entities.map((entity) => entity.json()) };
313 |     });
314 |     wsc.method('entities:components:add', (id, components) => {
315 |         const entity = api.entities.get(id);
316 |         if (!entity) {
317 |             return { error: 'Entity not found' };
318 |         }
319 |         Object.entries(components).forEach(([name, data]) => {
320 |             entity.addComponent(name, data);
321 |         });
322 |         log(`Added components(${Object.keys(components).join(', ')}) to entity(${id})`);
323 |         return { data: entity.json() };
324 |     });
325 |     wsc.method('entities:components:remove', (id, components) => {
326 |         const entity = api.entities.get(id);
327 |         if (!entity) {
328 |             return { error: 'Entity not found' };
329 |         }
330 |         components.forEach((component) => {
331 |             entity.removeComponent(component);
332 |         });
333 |         log(`Removed components(${components.join(', ')}) from entity(${id})`);
334 |         return { data: entity.json() };
335 |     });
336 |     wsc.method('entities:components:script:add', (id, scriptName) => {
337 |         const entity = api.entities.get(id);
338 |         if (!entity) {
339 |             return { error: 'Entity not found' };
340 |         }
341 |         if (!entity.get('components.script')) {
342 |             return { error: 'Script component not found' };
343 |         }
344 |         entity.addScript(scriptName);
345 |         log(`Added script(${scriptName}) to component(script) of entity(${id})`);
346 |         return { data: entity.get('components.script') };
347 |     });
348 | 
349 |     // assets
350 |     wsc.method('assets:create', async (assets) => {
351 |         try {
352 |             // Map each asset definition to a promise that handles its creation
353 |             const assetCreationPromises = assets.map(async ({ type, options }) => {
354 |                 if (options?.folder) {
355 |                     options.folder = api.assets.get(options.folder);
356 |                 }
357 | 
358 |                 let createPromise;
359 | 
360 |                 // Determine the correct API call based on the asset type
361 |                 switch (type) {
362 |                     case 'css':
363 |                         createPromise = api.assets.createCss(options);
364 |                         break;
365 |                     case 'folder':
366 |                         createPromise = api.assets.createFolder(options);
367 |                         break;
368 |                     case 'html':
369 |                         createPromise = api.assets.createHtml(options);
370 |                         break;
371 |                     case 'material':
372 |                         if (options?.data?.name) {
373 |                             options.name = options.data.name;
374 |                         }
375 |                         createPromise = api.assets.createMaterial(options);
376 |                         break;
377 |                     case 'script':
378 |                         createPromise = api.assets.createScript(options);
379 |                         break;
380 |                     case 'shader':
381 |                         createPromise = api.assets.createShader(options);
382 |                         break;
383 |                     case 'template':
384 |                         if (options?.entity) {
385 |                             options.entity = api.entities.get(options.entity);
386 |                         }
387 |                         createPromise = api.assets.createTemplate(options);
388 |                         break;
389 |                     case 'text':
390 |                         createPromise = api.assets.createText(options);
391 |                         break;
392 |                     default:
393 |                         // Throw an error for this specific promise if type is invalid
394 |                         throw new Error(`Invalid asset type: ${type}`);
395 |                 }
396 | 
397 |                 // Await the specific asset creation promise
398 |                 const asset = await createPromise;
399 | 
400 |                 // Check for creation failure and throw an error
401 |                 if (!asset) {
402 |                     throw new Error(`Failed to create asset of type ${type}`);
403 |                 }
404 | 
405 |                 // Log success and return the asset data for this promise
406 |                 log(`Created asset(${asset.get('id')}) - Type: ${type}`);
407 |                 return asset.json();
408 |             });
409 | 
410 |             // Wait for all creation promises to resolve concurrently
411 |             const createdAssetsData = await Promise.all(assetCreationPromises);
412 | 
413 |             // Return the collected data if all promises succeeded
414 |             return { data: createdAssetsData };
415 |         } catch (error) {
416 |             // Catch any error thrown during the mapping or from Promise.all
417 |             const errorMessage =
418 |                 error instanceof Error ? error.message : 'An unknown error occurred during asset creation.';
419 |             log(`Error creating assets: ${errorMessage}`);
420 |             return { error: errorMessage };
421 |         }
422 |     });
423 |     wsc.method('assets:delete', (ids) => {
424 |         const assets = ids.map((id) => api.assets.get(id));
425 |         if (!assets.length) {
426 |             return { error: 'Assets not found' };
427 |         }
428 |         api.assets.delete(assets);
429 |         log(`Deleted assets: ${ids.join(', ')}`);
430 |         return { data: true };
431 |     });
432 |     wsc.method('assets:list', (type) => {
433 |         let assets = api.assets.list();
434 |         if (type) {
435 |             assets = assets.filter((asset) => asset.get('type') === type);
436 |         }
437 |         log('Listed assets');
438 |         return { data: assets.map((asset) => asset.json()) };
439 |     });
440 |     wsc.method('assets:instantiate', async (ids) => {
441 |         const assets = ids.map((id) => api.assets.get(id));
442 |         if (!assets.length) {
443 |             return { error: 'Assets not found' };
444 |         }
445 |         if (assets.some((asset) => asset.get('type') !== 'template')) {
446 |             return { error: 'Invalid template asset' };
447 |         }
448 |         const entities = await api.assets.instantiateTemplates(assets);
449 |         log(`Instantiated assets: ${ids.join(', ')}`);
450 |         return { data: entities.map((entity) => entity.json()) };
451 |     });
452 |     wsc.method('assets:property:set', (id, prop, value) => {
453 |         const asset = api.assets.get(id);
454 |         if (!asset) {
455 |             return { error: 'Asset not found' };
456 |         }
457 |         asset.set(`data.${prop}`, value);
458 |         log(`Set asset(${id}) property(${prop}) to: ${JSON.stringify(value)}`);
459 |         return { data: asset.json() };
460 |     });
461 |     wsc.method('assets:script:text:set', async (id, text) => {
462 |         const asset = api.assets.get(id);
463 |         if (!asset) {
464 |             return { error: 'Asset not found' };
465 |         }
466 | 
467 |         const form = new FormData();
468 |         form.append('filename', asset.get('file.filename'));
469 |         form.append('file', new Blob([text], { type: 'text/plain' }), asset.get('file.filename'));
470 |         form.append('branchId', window.config.self.branch.id);
471 | 
472 |         try {
473 |             const data = await rest('PUT', `assets/${id}`, form, true);
474 |             if (data.error) {
475 |                 return { error: data.error };
476 |             }
477 |             log(`Set asset(${id}) script text`);
478 |             return { data };
479 |         } catch (e) {
480 |             return { error: e.message };
481 |         }
482 |     });
483 |     wsc.method('assets:script:parse', async (id) => {
484 |         const asset = api.assets.get(id);
485 |         if (!asset) {
486 |             return { error: 'Asset not found' };
487 |         }
488 |         // FIXME: This is a hacky way to get the parsed script data. Expose a proper API for this.
489 |         const [error, data] = await new Promise((resolve) => {
490 |             window.editor.call('scripts:parse', asset.observer, (...data) => resolve(data));
491 |         });
492 |         if (error) {
493 |             return { error };
494 |         }
495 |         if (Object.keys(data.scripts).length === 0) {
496 |             return { error: 'Failed to parse script' };
497 |         }
498 |         log(`Parsed asset(${id}) script`);
499 |         return { data };
500 |     });
501 | 
502 |     // scenes
503 |     wsc.method('scene:settings:modify', (settings) => {
504 |         const scene = api.settings.scene;
505 |         iterateObject(settings, (path, value) => {
506 |             scene.set(path, value);
507 |         });
508 | 
509 |         log('Modified scene settings');
510 |         return { data: scene.json() };
511 |     });
512 | 
513 |     // store
514 | 
515 |     // playcanvas
516 |     wsc.method('store:playcanvas:list', async (options = {}) => {
517 |         const params = [];
518 | 
519 |         if (options.search) {
520 |             params.push(`search=${options.search}`);
521 |         }
522 | 
523 |         params.push('regexp=true');
524 | 
525 |         if (options.order) {
526 |             params.push(`order=${options.order}`);
527 |         }
528 | 
529 |         if (options.skip) {
530 |             params.push(`skip=${options.skip}`);
531 |         }
532 | 
533 |         if (options.limit) {
534 |             params.push(`limit=${options.limit}`);
535 |         }
536 | 
537 |         try {
538 |             const data = await rest('GET', `store?${params.join('&')}`);
539 |             if (data.error) {
540 |                 return { error: data.error };
541 |             }
542 |             log(`Searched store: ${JSON.stringify(options)}`);
543 |             return { data };
544 |         } catch (e) {
545 |             return { error: e.message };
546 |         }
547 |     });
548 |     wsc.method('store:playcanvas:get', async (id) => {
549 |         try {
550 |             const data = await rest('GET', `store/${id}`);
551 |             if (data.error) {
552 |                 return { error: data.error };
553 |             }
554 |             log(`Got store item(${id})`);
555 |             return { data };
556 |         } catch (e) {
557 |             return { error: e.message };
558 |         }
559 |     });
560 |     wsc.method('store:playcanvas:clone', async (id, name, license) => {
561 |         try {
562 |             const data = await rest('POST', `store/${id}/clone`, {
563 |                 scope: {
564 |                     type: 'project',
565 |                     id: window.config.project.id
566 |                 },
567 |                 name,
568 |                 store: 'playcanvas',
569 |                 targetFolderId: null,
570 |                 license
571 |             });
572 |             if (data.error) {
573 |                 return { error: data.error };
574 |             }
575 |             log(`Cloned store item(${id})`);
576 |             return { data };
577 |         } catch (e) {
578 |             return { error: e.message };
579 |         }
580 |     });
581 | 
582 |     // sketchfab
583 |     wsc.method('store:sketchfab:list', async (options = {}) => {
584 |         const params = ['restricted=0', 'type=models', 'downloadable=true'];
585 | 
586 |         if (options.search) {
587 |             params.push(`q=${options.search}`);
588 |         }
589 | 
590 |         if (options.order) {
591 |             params.push(`sort_by=${options.order}`);
592 |         }
593 | 
594 |         if (options.skip) {
595 |             params.push(`cursor=${options.skip}`);
596 |         }
597 | 
598 |         if (options.limit) {
599 |             params.push(`count=${Math.min(options.limit ?? 0, 24)}`);
600 |         }
601 | 
602 |         try {
603 |             const res = await fetch(`https://api.sketchfab.com/v3/search?${params.join('&')}`);
604 |             const data = await res.json();
605 |             if (data.error) {
606 |                 return { error: data.error };
607 |             }
608 |             log(`Searched Sketchfab: ${JSON.stringify(options)}`);
609 |             return { data };
610 |         } catch (e) {
611 |             return { error: e.message };
612 |         }
613 |     });
614 |     wsc.method('store:sketchfab:get', async (uid) => {
615 |         try {
616 |             const res = await fetch(`https://api.sketchfab.com/v3/models/${uid}`);
617 |             const data = await res.json();
618 |             if (data.error) {
619 |                 return { error: data.error };
620 |             }
621 |             log(`Got Sketchfab model(${uid})`);
622 |             return { data };
623 |         } catch (e) {
624 |             return { error: e.message };
625 |         }
626 |     });
627 |     wsc.method('store:sketchfab:clone', async (uid, name, license) => {
628 |         try {
629 |             const data = await rest('POST', `store/${uid}/clone`, {
630 |                 scope: {
631 |                     type: 'project',
632 |                     id: window.config.project.id
633 |                 },
634 |                 name,
635 |                 store: 'sketchfab',
636 |                 targetFolderId: null,
637 |                 license
638 |             });
639 |             if (data.error) {
640 |                 return { error: data.error };
641 |             }
642 |             log(`Cloned sketchfab item(${uid})`);
643 |             return { data };
644 |         } catch (e) {
645 |             return { error: e.message };
646 |         }
647 |     });
648 | })();
649 | 
```

--------------------------------------------------------------------------------
/src/tools/schema/entity.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z, type ZodTypeAny } from 'zod';
  2 | 
  3 | import { AssetIdSchema, RgbSchema, RgbaSchema, Vec2Schema, Vec3Schema, Vec4Schema } from './common';
  4 | 
  5 | const AudioListenerSchema = z.object({
  6 |     enabled: z.boolean().optional().describe('Whether the component is enabled. Default: true')
  7 | }).describe('The data for the audio listener component.');
  8 | 
  9 | const CameraSchema = z.object({
 10 |     enabled: z.boolean().optional().describe('Whether the component is enabled. Default: true'),
 11 |     clearColorBuffer: z.boolean().optional().describe('If true, the camera will explicitly clear its render target to the chosen clear color before rendering the scene. Default: true'),
 12 |     clearColor: RgbaSchema.optional().describe('The color used to clear the camera\'s render target. Default: [0.118, 0.118, 0.118, 1]'),
 13 |     clearDepthBuffer: z.boolean().optional().describe('If true, the camera will explicitly clear the depth buffer of its render target before rendering the scene. Default: true'),
 14 |     renderSceneDepthMap: z.boolean().optional().describe('If true, the camera will render the scene depth map. Default: false'),
 15 |     renderSceneColorMap: z.boolean().optional().describe('If true, the camera will render the scene color map. Default: false'),
 16 |     projection: z.union([
 17 |         z.literal(0).describe('PROJECTION_PERSPECTIVE'),
 18 |         z.literal(1).describe('PROJECTION_ORTHOGRAPHIC')
 19 |     ]).optional().describe('The projection type of the camera. Default: 0 (PROJECTION_PERSPECTIVE)'),
 20 |     fov: z.number().optional().describe('The angle (in degrees) between top and bottom clip planes of a perspective camera. Default: 45'),
 21 |     frustumCulling: z.boolean().optional().describe('Controls the culling of mesh instances against the camera frustum. If true, culling is enabled. If false, all mesh instances in the scene are rendered by the camera, regardless of visibility. Default: true'),
 22 |     orthoHeight: z.number().optional().describe('The distance in world units between the top and bottom clip planes of an orthographic camera. Default: 4'),
 23 |     nearClip: z.number().min(0).optional().describe('The distance in camera space from the camera\'s eye point to the near plane. Default: 0.1'),
 24 |     farClip: z.number().min(0).optional().describe('The distance in camera space from the camera\'s eye point to the far plane. Default: 1000'),
 25 |     priority: z.number().optional().describe('A number that defines the order in which camera views are rendered by the engine. Smaller numbers are rendered first. Default: 0'),
 26 |     rect: Vec4Schema.optional().describe('An array of 4 numbers that represents the rectangle that specifies the viewport onto the camera\'s attached render target. This allows you to implement features like split-screen or picture-in-picture. It is defined by normalized coordinates (0 to 1) in the following format: [The lower left x coordinate, The lower left y coordinate, The width of the rectangle, The height of the rectangle]. Default: [0, 0, 1, 1]'),
 27 |     layers: z.array(z.number().int().min(0)).optional().describe('An array of layer id\'s that this camera will render. Default: [0, 1, 2, 3, 4]'),
 28 |     toneMapping: z.union([
 29 |         z.literal(0).describe('TONEMAP_LINEAR'),
 30 |         z.literal(1).describe('TONEMAP_FILMIC'),
 31 |         z.literal(2).describe('TONEMAP_HEJL'),
 32 |         z.literal(3).describe('TONEMAP_ACES'),
 33 |         z.literal(4).describe('TONEMAP_ACES2'),
 34 |         z.literal(5).describe('TONEMAP_NEUTRAL'),
 35 |         z.literal(6).describe('TONEMAP_NONE')
 36 |     ]).optional().describe('The tonemapping transform to apply to the final color of the camera. Default: 0 (TONEMAP_LINEAR)'),
 37 |     gammaCorrection: z.union([
 38 |         z.literal(0).describe('GAMMA_NONE'),
 39 |         z.literal(1).describe('GAMMA_SRGB')
 40 |     ]).optional().describe('The gamma correction to apply to the final color of the camera. Default: 1 (GAMMA_SRGB)')
 41 | }).describe('The data for the camera component.');
 42 | 
 43 | const CollisionSchema = z.object({
 44 |     enabled: z.boolean().optional().describe('Whether the component is enabled. Default: true'),
 45 |     type: z.enum(['box', 'sphere', 'capsule', 'cylinder', 'mesh']).optional().describe('The type of collision primitive. Can be: box, sphere, capsule, cylinder, mesh. Default: "box"'),
 46 |     halfExtents: Vec3Schema.optional().describe('The half-extents of the collision box. This is an array of 3 numbers: local space half-width, half-height, and half-depth. Default: [0.5, 0.5, 0.5]'),
 47 |     radius: z.number().min(0).optional().describe('The radius of the capsule/cylinder body. Default: 0.5'),
 48 |     axis: z.union([
 49 |         z.literal(0).describe('X'),
 50 |         z.literal(1).describe('Y'),
 51 |         z.literal(2).describe('Z')
 52 |     ]).optional().describe('Aligns the capsule/cylinder with the local-space X, Y or Z axis of the entity. Default: 1'),
 53 |     height: z.number().min(0).optional().describe('The tip-to-tip height of the capsule/cylinder. Default: 2'),
 54 |     convexHull: z.boolean().optional().describe('If true, the collision shape will be a convex hull. Default: false'),
 55 |     asset: AssetIdSchema.optional().describe('The `id` of the model asset that will be used as a source for the triangle-based collision mesh. Default: null'),
 56 |     renderAsset: AssetIdSchema.optional().describe('The `id` of the render asset that will be used as a source for the triangle-based collision mesh. Default: null'),
 57 |     linearOffset: Vec3Schema.optional().describe('The positional offset of the collision shape from the Entity position along the local axes. Default: [0, 0, 0]'),
 58 |     angularOffset: Vec3Schema.optional().describe('The rotational offset of the collision shape from the Entity rotation in local space. Default: [0, 0, 0]')
 59 | }).describe('The data for the collision component.');
 60 | 
 61 | const ElementSchema = z.object({
 62 |     enabled: z.boolean().optional().describe('Whether the component is enabled. Default: true'),
 63 |     type: z.enum(['text', 'image', 'group']).optional().describe('The type of the element. Default: "text"'),
 64 |     anchor: Vec4Schema.optional().describe('An array of 4 numbers controlling the left, bottom, right and top anchors of the element. Default: [0.5, 0.5, 0.5, 0.5]'),
 65 |     pivot: Vec2Schema.optional().describe('An array of 2 numbers controlling the origin of the element. Default: [0.5, 0.5]'),
 66 |     text: z.string().optional().describe('The text content of the element. Default: ""'),
 67 |     key: z.string().nullable().optional().describe('The localization key of the element. Default: null'),
 68 |     fontAsset: AssetIdSchema.optional().describe('The `id` of the font asset used by the element. Default: null'),
 69 |     fontSize: z.number().optional().describe('The size of the font used by the element. Default: 32'),
 70 |     minFontSize: z.number().optional().describe('The minimum size of the font when using `autoFitWidth` or `autoFitHeight`. Default: 8'),
 71 |     maxFontSize: z.number().optional().describe('The maximum size of the font when using `autoFitWidth` or `autoFitHeight`. Default: 32'),
 72 |     autoFitWidth: z.boolean().optional().describe('Automatically scale the font size to fit the element\'s width. Default: false'),
 73 |     autoFitHeight: z.boolean().optional().describe('Automatically scale the font size to fit the element\'s height. Default: false'),
 74 |     maxLines: z.number().nullable().optional().describe('The maximum number of lines that this element can display. Default: null'),
 75 |     lineHeight: z.number().optional().describe('The height of each line of text. Default: 32'),
 76 |     wrapLines: z.boolean().optional().describe('Automatically wrap lines based on the element width. Default: true'),
 77 |     spacing: z.number().optional().describe('The spacing between each letter of the text. Default: 1'),
 78 |     color: RgbSchema.optional().describe('The RGB color of the element. Default: [1, 1, 1]'),
 79 |     opacity: z.number().min(0).max(1).optional().describe('The opacity of the element. Default: 1'),
 80 |     textureAsset: AssetIdSchema.optional().describe('The `id` of the texture asset to be used by the element. Default: null'),
 81 |     spriteAsset: AssetIdSchema.optional().describe('The `id` of the sprite asset to be used by the element. Default: null'),
 82 |     spriteFrame: z.number().optional().describe('The frame from the sprite asset to render. Default: 0'),
 83 |     pixelsPerUnit: z.number().nullable().optional().describe('Number of pixels per PlayCanvas unit (used for 9-sliced sprites). Default: null'),
 84 |     width: z.number().optional().describe('The width of the element. Default: 32'),
 85 |     height: z.number().optional().describe('The height of the element. Default: 32'),
 86 |     margin: Vec4Schema.optional().describe('Spacing between each edge of the element and the respective anchor. Default: [-16, -16, -16, -16]'),
 87 |     alignment: Vec2Schema.optional().describe('Horizontal and vertical alignment of the text relative to its element transform. Default: [0.5, 0.5]'),
 88 |     outlineColor: RgbaSchema.optional().describe('Text outline effect color and opacity. Default: [0, 0, 0, 1]'),
 89 |     outlineThickness: z.number().optional().describe('Text outline effect width (0–1). Default: 0'),
 90 |     shadowColor: RgbaSchema.optional().describe('Text shadow color and opacity. Default: [0, 0, 0, 1]'),
 91 |     shadowOffset: Vec2Schema.optional().describe('Horizontal and vertical offset of the text shadow. Default: [0.0, 0.0]'),
 92 |     rect: Vec4Schema.optional().describe('Texture rect for the image element (u, v, width, height). Default: [0, 0, 1, 1]'),
 93 |     materialAsset: AssetIdSchema.optional().describe('The `id` of the material asset used by this element. Default: null'),
 94 |     autoWidth: z.boolean().optional().describe('Automatically size width to match text content. Default: false'),
 95 |     autoHeight: z.boolean().optional().describe('Automatically size height to match text content. Default: false'),
 96 |     fitMode: z.enum(['stretch', 'contain', 'cover']).optional().describe('Set how the content should be fitted and preserve the aspect ratio. Default: "stretch"'),
 97 |     useInput: z.boolean().optional().describe('Enable this to make the element respond to input events. Default: false'),
 98 |     batchGroupId: z.number().nullable().optional().describe('The batch group id that this element belongs to. Default: null'),
 99 |     mask: z.boolean().optional().describe('If true, this element acts as a mask for its children. Default: false'),
100 |     layers: z.array(z.number()).optional().describe('An array of layer ids that this element belongs to. Default: [4]'),
101 |     enableMarkup: z.boolean().optional().describe('Enable markup processing (only for text elements). Default: false')
102 | }).describe('The data for the element component.');
103 | 
104 | const LightSchema = z.object({
105 |     enabled: z.boolean().optional().describe('Whether the component is enabled. Default: true'),
106 |     type: z.enum(['directional', 'spot', 'omni']).optional().describe('The type of light. Can be: directional, spot, omni. Default: "directional"'),
107 |     bake: z.boolean().optional().describe('If true the light will be rendered into lightmaps. Default: false'),
108 |     bakeArea: z.number().min(0).max(180).optional().describe('If bake is enabled, specifies the directional light penumbra angle in degrees, allowing soft shadows. Default: 0'),
109 |     bakeNumSamples: z.number().int().min(1).max(255).optional().describe('If bake is enabled, specifies the number of samples used to bake this light into the lightmap. Default: 1'),
110 |     bakeDir: z.boolean().optional().describe('If true and `bake` is true, the light\'s direction will contribute to directional lightmaps. Default: true'),
111 |     affectDynamic: z.boolean().optional().describe('If true the light will affect non-lightmapped objects. Default: true'),
112 |     affectLightmapped: z.boolean().optional().describe('If true the light will affect lightmapped objects. Default: false'),
113 |     affectSpecularity: z.boolean().optional().describe('If true the light will affect material specularity. For directional light only. Default: true.'),
114 |     color: RgbSchema.optional().describe('An array of 3 numbers that represents the color of the emitted light. Default: [1, 1, 1]'),
115 |     intensity: z.number().min(0).max(32).optional().describe('The intensity of the light, this acts as a scalar value for the light\'s color. This value can exceed 1. Default: 1'),
116 |     castShadows: z.boolean().optional().describe('If true, the light will cause shadow casting models to cast shadows. Default: false'),
117 |     shadowUpdateMode: z.union([
118 |         z.literal(1).describe('SHADOWUPDATE_THISFRAME'),
119 |         z.literal(2).describe('SHADOWUPDATE_REALTIME')
120 |     ]).optional().describe('Tells the renderer how often shadows must be updated for this light. Default: 2 (SHADOWUPDATE_REALTIME)'),
121 |     shadowType: z.union([
122 |         z.literal(0).describe('SHADOW_PCF3_32F'),
123 |         z.literal(2).describe('SHADOW_VSM_16F'),
124 |         z.literal(3).describe('SHADOW_VSM_32F'),
125 |         z.literal(4).describe('SHADOW_PCF5_32F'),
126 |         z.literal(5).describe('SHADOW_PCF1_32F'),
127 |         z.literal(6).describe('SHADOW_PCSS_32F'),
128 |         z.literal(7).describe('SHADOW_PCF1_16F'),
129 |         z.literal(8).describe('SHADOW_PCF3_16F'),
130 |         z.literal(9).describe('SHADOW_PCF5_16F')
131 |     ]).optional().describe('Type of shadows being rendered by this light. Default: 0 (SHADOW_PCF3_32F)'),
132 |     vsmBlurMode: z.union([
133 |         z.literal(0).describe('BLUR_BOX'),
134 |         z.literal(1).describe('BLUR_GAUSSIAN')
135 |     ]).optional().describe('Blurring mode for variance shadow maps. Default: 1 (BLUR_GAUSSIAN)'),
136 |     vsmBlurSize: z.number().int().min(1).max(25).optional().describe('Number of samples used for blurring a variance shadow map. Only uneven numbers work, even are incremented. Minimum value is 1, maximum is 25. Default: 11'),
137 |     vsmBias: z.number().min(0).max(1).optional().describe('Constant depth offset applied to a shadow map to eliminate rendering artifacts like shadow acne. Default: 0.01'),
138 |     shadowDistance: z.number().min(0).optional().describe('The shadow distance is the maximum distance from the camera beyond which shadows from Directional Lights are no longer visible. Default: 16'),
139 |     shadowIntensity: z.number().min(0).optional().describe('The intensity of the shadow darkening, 1 being shadows are entirely black. Default: 1'),
140 |     shadowResolution: z.union([
141 |         z.literal(16).describe('16x16'),
142 |         z.literal(32).describe('32x32'),
143 |         z.literal(64).describe('64x64'),
144 |         z.literal(128).describe('128x128'),
145 |         z.literal(256).describe('256x256'),
146 |         z.literal(512).describe('512x512'),
147 |         z.literal(1024).describe('1024x1024'),
148 |         z.literal(2048).describe('2048x2048'),
149 |         z.literal(4096).describe('4096x4096')
150 |     ]).optional().describe('The size of the texture used for the shadow map (power of 2). Default: 1024'),
151 |     numCascades: z.number().int().min(1).max(4).optional().describe('Number of shadow cascades. Default: 1'),
152 |     cascadeDistribution: z.number().optional().describe('The distribution of subdivision of the camera frustum for individual shadow cascades. Default: 0.5'),
153 |     shadowBias: z.number().min(0).max(1).optional().describe('Constant depth offset applied to a shadow map to eliminate artifacts. Default: 0.2'),
154 |     normalOffsetBias: z.number().min(0).max(1).optional().describe('Normal offset depth bias. Default: 0.05'),
155 |     range: z.number().min(0).optional().describe('The distance from the spotlight source at which its contribution falls to zero. Default: 8'),
156 |     falloffMode: z.union([
157 |         z.literal(0).describe('LIGHTFALLOFF_LINEAR'),
158 |         z.literal(1).describe('LIGHTFALLOFF_INVERSESQUARED')
159 |     ]).optional().describe('Controls the rate at which a light attenuates from its position. Default: 0 (LIGHTFALLOFF_LINEAR)'),
160 |     innerConeAngle: z.number().min(0).max(45).optional().describe('The angle at which the spotlight cone starts to fade off (degrees). Affects spot lights only. Default: 40'),
161 |     outerConeAngle: z.number().min(0).max(90).optional().describe('The angle at which the spotlight cone has faded to nothing (degrees). Affects spot lights only. Default: 45'),
162 |     shape: z.union([
163 |         z.literal(0).describe('LIGHTSHAPE_PUNCTUAL'),
164 |         z.literal(1).describe('LIGHTSHAPE_RECT'),
165 |         z.literal(2).describe('LIGHTSHAPE_DISK'),
166 |         z.literal(3).describe('LIGHTSHAPE_SPHERE')
167 |     ]).optional().describe('The shape of the light source. Default: 0 (LIGHTSHAPE_PUNCTUAL)'),
168 |     cookieAsset: AssetIdSchema.optional().describe('The id of a texture asset that represents that light cookie. Default: null'),
169 |     cookieIntensity: z.number().min(0).max(1).optional().describe('Projection texture intensity. Default: 1.0'),
170 |     cookieFalloff: z.boolean().optional().describe('Toggle normal spotlight falloff when projection texture is used. Default: true'),
171 |     cookieChannel: z.enum(['r', 'g', 'b', 'a', 'rgb']).optional().describe('Color channels of the projection texture to use. Can be "r", "g", "b", "a", "rgb" or any swizzled combination. Default: "rgb"'),
172 |     cookieAngle: z.number().optional().describe('Angle for spotlight cookie rotation. Default: 0.0'),
173 |     cookieScale: Vec2Schema.optional().describe('Spotlight cookie scale. Default: [1.0, 1.0]'),
174 |     cookieOffset: Vec2Schema.optional().describe('Spotlight cookie position offset. Default: [0.0, 0.0]'),
175 |     isStatic: z.boolean().optional().describe('Mark light as non-movable (optimization). Default: false'),
176 |     layers: z.array(z.number().int().min(0)).optional().describe('An array of layer id\'s that this light will affect. Default: [0]')
177 | }).describe('The data for the light component.');
178 | 
179 | const RenderSchema = z.object({
180 |     enabled: z.boolean().optional().describe('Whether the component is enabled. Default: true'),
181 |     type: z.enum(['asset', 'box', 'capsule', 'sphere', 'cylinder', 'cone', 'plane']).optional().describe('The type of the render component. Can be: asset, box, capsule, cone, cylinder, plane, sphere. Default: "asset"'),
182 |     asset: z.number().int().nullable().optional().describe('The `id` of the render asset for the render component (only applies to type "asset"). Default: null'),
183 |     materialAssets: z.array(z.number().int().nullable()).optional().describe('An array of material asset `id`s that will be used to render the meshes. Each material corresponds to the respective mesh instance. Default: []'),
184 |     layers: z.array(z.number().int().min(0)).optional().describe('An array of layer id\'s to which the meshes should belong. Default: [0]'),
185 |     batchGroupId: z.number().int().nullable().optional().describe('The batch group id that the meshes should belong to. Default: null'),
186 |     castShadows: z.boolean().optional().describe('If true, attached meshes will cast shadows for lights that have shadow casting enabled. Default: true'),
187 |     castShadowsLightmap: z.boolean().optional().describe('If true, the meshes will cast shadows when rendering lightmaps. Default: true'),
188 |     receiveShadows: z.boolean().optional().describe('If true, shadows will be cast on attached meshes. Default: true'),
189 |     lightmapped: z.boolean().optional().describe('If true, the meshes will be lightmapped after using lightmapper.bake(). Default: false'),
190 |     lightmapSizeMultiplier: z.number().optional().describe('Lightmap resolution multiplier. Default: 1.0'),
191 |     isStatic: z.boolean().optional().describe('Mark meshes as non-movable (optimization). Default: false'),
192 |     rootBone: z.string().nullable().optional().describe('The `resource_id` of the entity to be used as the root bone for any skinned meshes that are rendered by this component. Default: null'),
193 |     aabbCenter: Vec3Schema.optional().describe('An array of 3 numbers controlling the center of the AABB to be used. Default: [0, 0, 0]'),
194 |     aabbHalfExtents: Vec3Schema.optional().describe('An array of 3 numbers controlling the half extents of the AABB to be used. Default: [0.5, 0.5, 0.5]')
195 | }).describe('The data for the render component.');
196 | 
197 | const RigidBodySchema = z.object({
198 |     enabled: z.boolean().optional().describe('Whether the component is enabled. Default: true'),
199 |     type: z.enum(['static', 'dynamic', 'kinematic']).optional().describe('The type of RigidBody determines how it is simulated. Can be one of: static, dynamic, kinematic. Default: "static"'),
200 |     mass: z.number().min(0).optional().describe('The mass of the body. Default: 1'),
201 |     linearDamping: z.number().min(0).max(1).optional().describe('Controls the rate at which a body loses linear velocity over time. Default: 0'),
202 |     angularDamping: z.number().min(0).max(1).optional().describe('Controls the rate at which a body loses angular velocity over time. Default: 0'),
203 |     linearFactor: Vec3Schema.optional().describe('An array of 3 numbers that represents the scaling factor for linear movement of the body in each axis. Default: [1, 1, 1]'),
204 |     angularFactor: Vec3Schema.optional().describe('An array of 3 numbers that represents the scaling factor for angular movement of the body in each axis. Default: [1, 1, 1]'),
205 |     friction: z.number().min(0).max(1).optional().describe('The friction value used when contacts occur between two bodies. Default: 0.5'),
206 |     restitution: z.number().min(0).max(1).optional().describe('The amount of energy lost when two objects collide, this determines the bounciness of the object. Default: 0.5')
207 | }).describe('The data for the rigidbody component.');
208 | 
209 | const ScreenSchema = z.object({
210 |     enabled: z.boolean().optional().describe('Whether the component is enabled. Default: true'),
211 |     screenSpace: z.boolean().optional().describe('If true then the screen will display its child Elements in 2D. Set this to false to make this a 3D screen. Default: true'),
212 |     scaleMode: z.enum(['none', 'blend']).optional().describe('Controls how a screen-space screen is resized when the window size changes. Can be: `pc.SCALEMODE_BLEND`: Use it to have the screen adjust between the difference of the window resolution and the screen\'s reference resolution. `pc.SCALEMODE_NONE`: Use it to make the screen always have a size equal to its resolution. Default: "blend"'),
213 |     scaleBlend: z.number().min(0).max(1).optional().describe('Set this to 0 to only adjust to changes between the width of the window and the x of the reference resolution. Set this to 1 to only adjust to changes between the window height and the y of the reference resolution. A value in the middle will try to adjust to both. Default: 0.5'),
214 |     resolution: Vec2Schema.optional().describe('An array of 2 numbers that represents the resolution of the screen. Default: [1280, 720]'),
215 |     referenceResolution: Vec2Schema.optional().describe('An array of 2 numbers that represents the reference resolution of the screen. If the window size changes the screen will adjust its size based on `scaleMode` using the reference resolution. Default: [1280, 720]'),
216 |     priority: z.number().int().min(0).max(127).optional().describe('Determines the order in which Screen components in the same layer are rendered (higher priority is rendered on top). Number must be an integer between 0 and 127. Default: 0')
217 | }).describe('The data for the screen component.');
218 | 
219 | const ScriptAttributeSchema = z.any().describe('A dictionary that holds the values of each attribute. The keys in the dictionary are the attribute names.');
220 | 
221 | const ScriptInstanceSchema = z.object({
222 |     enabled: z.boolean().optional().describe('Whether the script instance is enabled. Default: true'),
223 |     attributes: z.record(ScriptAttributeSchema).optional().describe('A dictionary that holds the values of each attribute. The keys in the dictionary are the attribute names. Default: {}')
224 | });
225 | 
226 | const ScriptSchema = z.object({
227 |     enabled: z.boolean().optional().describe('Whether the component is enabled. Default: true'),
228 |     order: z.array(z.string()).optional().describe('An array of script names in the order in which they should be executed at runtime. Default: []'),
229 |     scripts: z.record(ScriptInstanceSchema).optional().describe('A dictionary that contains all the scripts attached to this script component. Each key in the dictionary is the script name. Default: {}')
230 | }).describe('The data for the script component.');
231 | 
232 | const SoundSlotSchema = z.object({
233 |     name: z.string().optional().describe('The name of the sound slot. Default: "Slot 1"'),
234 |     volume: z.number().min(0).max(1).optional().describe('The volume modifier to play the audio with. Default: 1'),
235 |     pitch: z.number().min(0).optional().describe('The pitch to playback the audio at. A value of 1 means the audio is played back at the original pitch. Default: 1'),
236 |     asset: AssetIdSchema.optional().describe('The `id` of the audio asset that can be played from this sound slot. Default: null'),
237 |     startTime: z.number().optional().describe('The start time from which the sound will start playing. Default: 0'),
238 |     duration: z.number().nullable().optional().describe('The duration of the sound that the slot will play starting from startTime. Default: null'),
239 |     loop: z.boolean().optional().describe('If true, the slot will loop playback continuously. Otherwise, it will be played once to completion. Default: false'),
240 |     autoPlay: z.boolean().optional().describe('If true, the slot will be played on load. Otherwise, sound slots will need to be played by scripts. Default: false'),
241 |     overlap: z.boolean().optional().describe('If true then sounds played from slot will be played independently of each other. Otherwise the slot will first stop the current sound before starting the new one. Default: false')
242 | }).partial();
243 | 
244 | const SoundSchema = z.object({
245 |     enabled: z.boolean().optional().describe('Whether the component is enabled. Default: true'),
246 |     volume: z.number().min(0).max(1).optional().describe('The volume modifier to play the audio with. The volume of each slot is multiplied with this value. Default: 1'),
247 |     pitch: z.number().min(0).optional().describe('The pitch to playback the audio at. A value of 1 means the audio is played back at the original pitch. The pitch of each slot is multiplied with this value. Default: 1'),
248 |     positional: z.boolean().optional().describe('If true, the component will play back audio assets as if played from the location of the entity in 3D space. Default: true'),
249 |     refDistance: z.number().min(0).optional().describe('The reference distance for reducing volume as the sound source moves further from the listener. Default: 1'),
250 |     maxDistance: z.number().min(0).optional().describe('The maximum distance from the listener at which audio falloff stops. Note the volume of the audio is not 0 after this distance, but just doesn\'t fall off anymore. Default: 10000'),
251 |     rollOffFactor: z.number().min(0).optional().describe('The rate at which volume fall-off occurs. Default: 1'),
252 |     distanceModel: z.enum(['linear', 'inverse', 'exponential']).optional().describe('Determines which algorithm to use to reduce the volume of the audio as it moves away from the listener. Can be one of: "inverse", "linear", "exponential". Default: "linear"'),
253 |     slots: z.record(SoundSlotSchema).default({
254 |         '1': {
255 |             name: 'Slot 1',
256 |             loop: false,
257 |             autoPlay: false,
258 |             overlap: false,
259 |             asset: null,
260 |             startTime: 0,
261 |             duration: null,
262 |             volume: 1,
263 |             pitch: 1
264 |         }
265 |     }).describe('A dictionary of sound slots. Each sound slot controls playback of an audio asset. Each key in the dictionary is a number representing the index of each sound slot.')
266 | }).describe('The data for the sound component.');
267 | 
268 | export const ComponentsSchema = z.object({
269 |     audiolistener: AudioListenerSchema.optional(),
270 |     camera: CameraSchema.optional(),
271 |     collision: CollisionSchema.optional(),
272 |     element: ElementSchema.optional(),
273 |     light: LightSchema.optional(),
274 |     render: RenderSchema.optional(),
275 |     rigidbody: RigidBodySchema.optional(),
276 |     screen: ScreenSchema.optional(),
277 |     script: ScriptSchema.optional(),
278 |     sound: SoundSchema.optional()
279 | }).describe('A dictionary that contains the components of the entity and their data.');
280 | 
281 | export const ComponentNameSchema = z.enum([
282 |     'anim',
283 |     'animation',
284 |     'audiolistener',
285 |     'button',
286 |     'camera',
287 |     'collision',
288 |     'element',
289 |     'layoutchild',
290 |     'layoutgroup',
291 |     'light',
292 |     'model',
293 |     'particlesystem',
294 |     'render',
295 |     'rigidbody',
296 |     'screen',
297 |     'script',
298 |     'scrollbar',
299 |     'scrollview',
300 |     'sound',
301 |     'sprite'
302 | ]);
303 | 
304 | export const EntitySchema: z.ZodOptional<ZodTypeAny> = z.lazy(() => z.object({
305 |     name: z.string().optional().describe('The name of the entity. Default: "Untitled"'),
306 |     enabled: z.boolean().optional().describe('Whether the entity is enabled. Default: true'),
307 |     tags: z.array(z.string()).optional().describe('The tags of the entity. Default: []'),
308 |     children: z.array(EntitySchema).optional().describe('An array that contains the child entities. Default: []'),
309 |     position: Vec3Schema.optional().describe('The position of the entity in local space (x, y, z). Default: [0, 0, 0]'),
310 |     rotation: Vec3Schema.optional().describe('The rotation of the entity in local space (rx, ry, rz euler angles in degrees). Default: [0, 0, 0]'),
311 |     scale: Vec3Schema.optional().describe('The scale of the entity in local space (sx, sy, sz). Default: [1, 1, 1]'),
312 |     components: ComponentsSchema.optional().describe('The components of the entity and their data. Default: {}')
313 | })).optional();
314 | 
```