create-playcanvas 0.15.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -24
- package/dist/index.mjs +11 -11
- package/package.json +1 -1
- package/templates/_shared/scene-editor/AGENTS.md +8 -0
- package/templates/_shared/scene-editor/README.md +15 -2
- package/templates/_shared/scene-editor/src/scene-editor.ts +186 -36
- package/templates/engine/ar-placement/index.html +20 -0
- package/templates/engine/ar-placement/src/main.ts +22 -12
- package/templates/engine/base/AGENTS.md +1 -0
- package/templates/engine/base/README.md +1 -1
- package/templates/engine/first-person-controller/index.html +19 -0
- package/templates/engine/first-person-controller/src/main.ts +16 -11
- package/templates/engine/product-configurator/index.html +30 -0
- package/templates/engine/product-configurator/src/main.ts +33 -32
- package/templates/engine/scene-editor/index.html +33 -0
- package/templates/engine/scene-editor/src/main.ts +56 -47
- package/templates/engine/sprite-game/index.html +19 -0
- package/templates/engine/sprite-game/src/main.ts +19 -11
- package/templates/engine/third-person-controller/index.html +19 -0
- package/templates/engine/third-person-controller/src/main.ts +16 -11
- package/templates/engine/vr-starter/index.html +20 -0
- package/templates/engine/vr-starter/src/main.ts +22 -12
- package/templates/index.ts +1 -14
- package/templates/react/base/AGENTS.md +1 -0
- package/templates/react/scene-editor/src/App.tsx +73 -40
- package/templates/web-components/base/AGENTS.md +1 -0
- package/templates/web-components/scene-editor/index.html +32 -25
- package/templates/web-components/scene-editor/src/main.ts +33 -11
- package/templates/_shared/interactive-sphere/README.md +0 -7
- package/templates/_shared/interactive-sphere/public/environment-map.png +0 -0
- package/templates/_shared/interactive-sphere/src/counter.css +0 -19
- package/templates/_shared/physics-playground/README.md +0 -7
- package/templates/engine/interactive-sphere/src/App.ts +0 -182
- package/templates/engine/interactive-sphere/src/main.ts +0 -16
- package/templates/engine/interactive-sphere/src/utils.ts +0 -16
- package/templates/engine/physics-playground/src/main.ts +0 -131
- package/templates/react/interactive-sphere/src/App.tsx +0 -21
- package/templates/react/interactive-sphere/src/Scene.tsx +0 -70
- package/templates/react/physics-playground/src/App.tsx +0 -85
- package/templates/web-components/interactive-sphere/index.html +0 -36
- package/templates/web-components/interactive-sphere/src/main.ts +0 -76
- package/templates/web-components/physics-playground/index.html +0 -46
- package/templates/web-components/physics-playground/src/main.ts +0 -32
package/README.md
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
[](https://www.reddit.com/r/PlayCanvas)
|
|
8
8
|
[](https://x.com/intent/follow?screen_name=playcanvas)
|
|
9
9
|
|
|
10
|
-
| [Engine Manual](https://developer.playcanvas.com/user-manual/engine/) | [React Manual](https://developer.playcanvas.com/user-manual/react/) | [Examples](https://playcanvas.
|
|
10
|
+
| [Engine Manual](https://developer.playcanvas.com/user-manual/engine/) | [React Manual](https://developer.playcanvas.com/user-manual/react/) | [Web Components Manual](https://developer.playcanvas.com/user-manual/web-components/) | [Examples](https://playcanvas.com/examples) | [Forum](https://forum.playcanvas.com/) |
|
|
11
11
|
|
|
12
12
|
Scaffold a Vite-powered PlayCanvas project with TypeScript. Pick a format and a runnable starter, then build from there.
|
|
13
13
|
|
|
14
|
-
## Getting
|
|
14
|
+
## Getting started
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
# npm
|
|
@@ -28,17 +28,31 @@ Then follow the prompts. Requires Node.js 22.23.2 or later.
|
|
|
28
28
|
|
|
29
29
|
## Formats
|
|
30
30
|
|
|
31
|
-
| Format
|
|
32
|
-
|
|
|
33
|
-
| `engine`
|
|
34
|
-
| `react`
|
|
35
|
-
| `web-components` | `@playcanvas/web-components` custom elements |
|
|
31
|
+
| Format | Description |
|
|
32
|
+
| ---------------------------------------------------------------- | -------------------------------------------- |
|
|
33
|
+
| [`engine`](https://github.com/playcanvas/engine) | The PlayCanvas Engine API directly |
|
|
34
|
+
| [`react`](https://github.com/playcanvas/react) | `@playcanvas/react` components |
|
|
35
|
+
| [`web-components`](https://github.com/playcanvas/web-components) | `@playcanvas/web-components` custom elements |
|
|
36
36
|
|
|
37
|
-
Every format uses TypeScript and includes Vite, ESLint
|
|
37
|
+
Every format uses TypeScript and includes Vite with hot reload, ESLint and Prettier. The development server loads the [PlayCanvas Inspector](https://github.com/playcanvas/inspector), a debug panel for the running scene, and production builds use relative paths, so they run from any folder, such as a GitHub Pages project site or an itch.io upload.
|
|
38
38
|
|
|
39
39
|
## Starters
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Every starter is a runnable scene, available in every format. Run the creator to browse them, or pass `--help` to list them; `spinning-cube` is the default.
|
|
42
|
+
|
|
43
|
+
| Starter | Category | Description |
|
|
44
|
+
| ------------------------- | -------- | --------------------------------------------------------------- |
|
|
45
|
+
| `spinning-cube` | Basics | A rotating cube and a light |
|
|
46
|
+
| `model-viewer` | Viewers | A glTF model, orbit controls and a procedural sky |
|
|
47
|
+
| `splat-viewer` | Viewers | A Gaussian splat and orbit controls |
|
|
48
|
+
| `product-configurator` | Viewers | Product variants, materials and orbit controls |
|
|
49
|
+
| `first-person-controller` | Games | First-person movement, mouse look and jumping |
|
|
50
|
+
| `third-person-controller` | Games | An animated character, follow camera and locomotion |
|
|
51
|
+
| `sprite-game` | Games | An orthographic camera, sprite animation and input |
|
|
52
|
+
| `web-lens` | Games | A portrait game with Snapchat leaderboards, packaged for upload |
|
|
53
|
+
| `scene-editor` | Tools | Click to select, gizmos and a scene saved in the browser |
|
|
54
|
+
| `vr-starter` | XR | Immersive VR, controllers and locomotion |
|
|
55
|
+
| `ar-placement` | XR | Mobile AR, hit testing and object placement |
|
|
42
56
|
|
|
43
57
|

|
|
44
58
|
|
|
@@ -52,7 +66,7 @@ npm create playcanvas@latest my-lens -- --starter web-lens
|
|
|
52
66
|
|
|
53
67
|
Its `npm run package` script builds the game, checks it against the Web Lens upload rules (size limits, relative paths, no network access and no camera or microphone) and writes the zip to upload through [My Lenses](https://my-lenses.snapchat.com/).
|
|
54
68
|
|
|
55
|
-
## CLI
|
|
69
|
+
## CLI options
|
|
56
70
|
|
|
57
71
|
Pass a project name and options to skip the prompts:
|
|
58
72
|
|
|
@@ -60,35 +74,57 @@ Pass a project name and options to skip the prompts:
|
|
|
60
74
|
npm create playcanvas@latest my-game -- -f react -y
|
|
61
75
|
```
|
|
62
76
|
|
|
63
|
-
|
|
64
|
-
| ------------------ | --------- | ------------------------------------------------------- |
|
|
65
|
-
| `--format <name>` | `-f` | Use a format from the list above |
|
|
66
|
-
| `--starter <name>` | `-s` | Use a starter from the list above |
|
|
67
|
-
| `--overwrite` | | Remove existing files from a non-empty target directory |
|
|
68
|
-
| `--keep` | | Add the project to a non-empty target directory |
|
|
69
|
-
| `--no-skills` | | Omit the PlayCanvas agent skills (included by default) |
|
|
70
|
-
| `--yes` | `-y` | Skip the prompts and take the defaults |
|
|
71
|
-
| `--help` | `-h` | Show command help, with the formats and starters |
|
|
77
|
+
npm needs the `--` before the options. pnpm, yarn and bun take them without it, and may ignore them after one:
|
|
72
78
|
|
|
73
|
-
|
|
79
|
+
```bash
|
|
80
|
+
pnpm create playcanvas@latest my-game -f react -y
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Use `.` as the project name to create the project in the current directory.
|
|
84
|
+
|
|
85
|
+
| Option | Shorthand | Description |
|
|
86
|
+
| ------------------ | --------- | ----------------------------------------------------------------------- |
|
|
87
|
+
| `--format <name>` | `-f` | Use a format from the list above |
|
|
88
|
+
| `--starter <name>` | `-s` | Use a starter from the list above |
|
|
89
|
+
| `--overwrite` | | Remove existing files, except `.git`, from a non-empty target directory |
|
|
90
|
+
| `--keep` | | Add the project to a non-empty target directory |
|
|
91
|
+
| `--no-skills` | | Omit the PlayCanvas agent skills (included by default) |
|
|
92
|
+
| `--yes` | `-y` | Skip the prompts and take the defaults |
|
|
93
|
+
| `--help` | `-h` | Show command help, with the formats and starters |
|
|
94
|
+
|
|
95
|
+
`--yes` takes `playcanvas-project`, the `engine` format and the `spinning-cube` starter for anything you don't pass. It never touches files that are already there, so a non-empty target directory still needs `--keep` or `--overwrite`. A run without a terminal, such as one started by an AI agent or in CI, takes the same defaults, so it never waits on a prompt. The previous `--template` (`-t`) and `--boilerplate` (`-b`) flags remain accepted for compatibility.
|
|
74
96
|
|
|
75
|
-
`--keep` adds the project to a directory that already holds files, such as a brief, assets or a README, and replaces none of them: where the project has a file of the same name, yours stays and the creator lists it. It
|
|
97
|
+
`--keep` adds the project to a directory that already holds files, such as a brief, assets or a README, and replaces none of them: where the project has a file of the same name, yours stays and the creator lists it. It refuses a directory that already has a `package.json`, since that is a project already. To add a project to the current directory:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm create playcanvas@latest . -- --keep
|
|
101
|
+
```
|
|
76
102
|
|
|
77
103
|
## Coding agents
|
|
78
104
|
|
|
79
|
-
Every project includes an `AGENTS.md` that gives AI coding agents such as Claude Code, Codex and Cursor
|
|
105
|
+
Every project includes an `AGENTS.md` that gives AI coding agents such as Claude Code, Codex and Cursor the project's commands, layout and conventions, and how to check a change. The project's `CLAUDE.md` imports `AGENTS.md`, since Claude Code skips `AGENTS.md` when a `CLAUDE.md` exists in the working directory or a folder above it.
|
|
80
106
|
|
|
81
107
|
Every project also includes [`@playcanvas/skills`](https://github.com/playcanvas/skills), so those agents pick up PlayCanvas-specific workflows automatically, with no install step. They are copied into `.claude/skills/` and `.agents/skills/`. Pass `--no-skills` to leave them out.
|
|
82
108
|
|
|
109
|
+
Coding agents: start from the [PlayCanvas documentation index](https://developer.playcanvas.com/llms.txt), which leads to a Markdown version of every guide and API page.
|
|
110
|
+
|
|
83
111
|
## Development
|
|
84
112
|
|
|
85
113
|
```bash
|
|
86
114
|
npm install
|
|
87
|
-
npm test
|
|
88
115
|
npm run build
|
|
89
116
|
node dist/index.mjs
|
|
90
117
|
```
|
|
91
118
|
|
|
119
|
+
`npm run dev` builds a stub that runs the creator straight from `src/`, so a change takes effect without a rebuild. Before opening a pull request, run the checks CI runs:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm test
|
|
123
|
+
npm run lint
|
|
124
|
+
npm run fmt
|
|
125
|
+
npm run typecheck
|
|
126
|
+
```
|
|
127
|
+
|
|
92
128
|
Contributions are welcome. Please open an issue before proposing a new format, starter or another substantial change.
|
|
93
129
|
|
|
94
130
|
### Adding a starter
|
|
@@ -101,7 +137,16 @@ A project is assembled from the layers under `templates/`, in this order. A file
|
|
|
101
137
|
4. `_features/<feature>/<layer>/`: what each of the starter's `features` needs in that format, as mapped by `FEATURES` in `templates/index.ts`
|
|
102
138
|
5. `<format>/<starter>/`: the starter's scene in that format
|
|
103
139
|
|
|
104
|
-
To add a starter
|
|
140
|
+
To add a starter:
|
|
141
|
+
|
|
142
|
+
1. List it in `STARTERS` in `templates/index.ts`, with the `name`, `display`, `category` and `description` that the prompts and `--help` show. The first entry is the default. A starter can also name the `features` it uses, the files and folders of `_common/` and the format bases to `omit`, and an `outro` to follow the commands in the closing message.
|
|
143
|
+
2. Add `_shared/<starter>/README.md`, a `##` section that describes the starter and its controls.
|
|
144
|
+
3. Add its scene to every format: `engine/<starter>/src/main.ts`, `react/<starter>/src/App.tsx`, and `web-components/<starter>/index.html` with `src/main.ts`. Import `./starter.css` for the shared page and panel styles, and override a base file only when the starter needs something different.
|
|
145
|
+
4. Add it to the table under [Starters](#starters) and to `images/starter-catalog.webp`.
|
|
146
|
+
|
|
147
|
+
`npm test` checks that the starter exists in every format, and CI scaffolds it in each one, then checks its formatting, lints, typechecks and builds it.
|
|
148
|
+
|
|
149
|
+
Name a layer's `.gitignore` or `.npmrc` as `_gitignore` or `_npmrc`, since npm leaves those two out of a published package. The scaffolder renames them.
|
|
105
150
|
|
|
106
151
|
Write commands in a README or `AGENTS.md` for npm, as `npm install`, `npm run <script>` and `npm run <script> -- <args>`. The scaffolder rewrites them for the package manager that created the project, and leaves out the `## Agent skills` section of both when run with `--no-skills`.
|
|
107
152
|
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{styleText as u}from"node:util";import
|
|
2
|
+
import{styleText as u}from"node:util";import B,{stdin as Ue,stdout as ye}from"node:process";import Ge from"node:readline";import"node:tty";import y from"node:fs";import b from"node:path";import Ke from"node:os";import{fileURLToPath as He}from"node:url";const ze=(()=>{const e=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g;return t=>{let r=0;for(e.lastIndex=0;e.test(t);)r+=1;return t.length-r}})(),Je=e=>e===12288||e>=65281&&e<=65376||e>=65504&&e<=65510,Ye=e=>e===8987||e===9001||e>=12272&&e<=12287||e>=12289&&e<=12350||e>=12441&&e<=12543||e>=12549&&e<=12591||e>=12593&&e<=12686||e>=12688&&e<=12771||e>=12783&&e<=12830||e>=12832&&e<=12871||e>=12880&&e<=19903||e>=65040&&e<=65049||e>=65072&&e<=65106||e>=65108&&e<=65126||e>=65128&&e<=65131||e>=127488&&e<=127490||e>=127504&&e<=127547||e>=127552&&e<=127560||e>=131072&&e<=196605||e>=196608&&e<=262141,qe=/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]|\u001b\]8;[^;]*;.*?(?:\u0007|\u001b\u005c)/y,Xe=/[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y,$e=/(?:(?![\uFF61-\uFF9F\uFF00-\uFFEF])[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Tangut}]){1,1000}/yu,Qe=/\t{1,1000}/y,we=new RegExp("[\\u{1F1E6}-\\u{1F1FF}]{2}|\\u{1F3F4}[\\u{E0061}-\\u{E007A}]{2}[\\u{E0030}-\\u{E0039}\\u{E0061}-\\u{E007A}]{1,3}\\u{E007F}|(?:\\p{Emoji}\\uFE0F\\u20E3?|\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation})(?:\\u200D(?:\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F\\u20E3?))*","yu"),Ze=/(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y,et=new RegExp("\\p{M}+","gu"),tt={limit:1/0,ellipsis:""},xe=(e,t={},r={})=>{const i=t.limit??1/0,n=t.ellipsis??"",o=t?.ellipsisWidth??(n?xe(n,tt,r).width:0),l=0,a=r.controlWidth??0,s=r.tabWidth??8,c=r.emojiWidth??2,d=2,h=r.regularWidth??1,m=r.wideWidth??d,f=[[Ze,h],[qe,l],[Xe,a],[Qe,s],[we,c],[$e,m]];let g=0,w=0,_=e.length,F=0,j=!1,M=_,O=Math.max(0,i-o),v=0,C=0,$=0,I=0;e:for(;;){if(C>v||w>=_&&w>g){const R=e.slice(v,C)||e.slice(g,w);F=0;for(const D of R.replaceAll(et,"")){const A=D.codePointAt(0)||0;if(Je(A)?I=d:Ye(A)?I=m:I=h,$+I>O&&(M=Math.min(M,Math.max(v,g)+F)),$+I>i){j=!0;break e}F+=D.length,$+=I}v=C=0}if(w>=_)break e;for(let R=0,D=f.length;R<D;R++){const[A,H]=f[R];if(A.lastIndex=w,A.test(e)){if(F=A===$e?ze(e.slice(w,A.lastIndex)):A===we?1:A.lastIndex-w,I=F*H,$+I>O&&(M=Math.min(M,w+Math.floor((O-$)/H))),$+I>i){j=!0;break e}$+=I,v=g,C=w,w=g=A.lastIndex;continue e}}w+=1}return{width:j?O:$,index:j?M:_,truncated:j,ellipsed:j&&i>=o}},rt={limit:1/0,ellipsis:"",ellipsisWidth:0},W=(e,t={})=>xe(e,rt,t).width,Z="\x1B",ve="\x9B",it=39,ce="\x07",Se="[",st="]",Ee="m",ue=`${st}8;;`,_e=new RegExp(`(?:\\${Se}(?<code>\\d+)m|\\${ue}(?<uri>.*)${ce})`,"y"),Ie=e=>{if(e>=30&&e<=37||e>=90&&e<=97)return 39;if(e>=40&&e<=47||e>=100&&e<=107)return 49;if(e===1||e===2)return 22;if(e===3)return 23;if(e===4)return 24;if(e===7)return 27;if(e===8)return 28;if(e===9)return 29;if(e===0)return 0},Fe=e=>`${Z}${Se}${e}${Ee}`,Ce=e=>`${Z}${ue}${e}${ce}`,he=(e,t,r)=>{const i=t[Symbol.iterator]();let n=!1,o=!1,l=e.at(-1),a=l===void 0?0:W(l),s=i.next(),c=i.next(),d=0;for(;!s.done;){const h=s.value,m=W(h);a+m<=r?e[e.length-1]+=h:(e.push(h),a=0),(h===Z||h===ve)&&(n=!0,o=t.startsWith(ue,d+1)),n?o?h===ce&&(n=!1,o=!1):h===Ee&&(n=!1):(a+=m,a===r&&!c.done&&(e.push(""),a=0)),s=c,c=i.next(),d+=h.length}l=e.at(-1),!a&&l!==void 0&&l.length&&e.length>1&&(e[e.length-2]+=e.pop())},nt=e=>{const t=e.split(" ");let r=t.length;for(;r&&!W(t[r-1]);)r--;return r===t.length?e:t.slice(0,r).join(" ")+t.slice(r).join("")},at=(e,t,r={})=>{if(r.trim!==!1&&e.trim()==="")return"";let i="",n,o;const l=e.split(" ");let a=[""],s=0;for(let h=0;h<l.length;h++){const m=l[h];if(r.trim!==!1){const g=a.at(-1)??"",w=g.trimStart();g.length!==w.length&&(a[a.length-1]=w,s=W(w))}h!==0&&(s>=t&&(r.wordWrap===!1||r.trim===!1)&&(a.push(""),s=0),(s||r.trim===!1)&&(a[a.length-1]+=" ",s++));const f=W(m);if(r.hard&&f>t){const g=t-s,w=1+Math.floor((f-g-1)/t);Math.floor((f-1)/t)<w&&a.push(""),he(a,m,t),s=W(a.at(-1)??"");continue}if(s+f>t&&s&&f){if(r.wordWrap===!1&&s<t){he(a,m,t),s=W(a.at(-1)??"");continue}a.push(""),s=0}if(s+f>t&&r.wordWrap===!1){he(a,m,t),s=W(a.at(-1)??"");continue}a[a.length-1]+=m,s+=f}r.trim!==!1&&(a=a.map(h=>nt(h)));const c=a.join(`
|
|
3
3
|
`);let d=!1;for(let h=0;h<c.length;h++){const m=c[h];if(i+=m,d)d=!1;else if(d=m>="\uD800"&&m<="\uDBFF",d)continue;if(m===Z||m===ve){_e.lastIndex=h+1;const g=_e.exec(c)?.groups;if(g?.code!==void 0){const w=Number.parseFloat(g.code);n=w===it?void 0:w}else g?.uri!==void 0&&(o=g.uri.length===0?void 0:g.uri)}if(c[h+1]===`
|
|
4
4
|
`){o&&(i+=Ce(""));const f=n?Ie(n):void 0;n&&f&&(i+=Fe(f))}else m===`
|
|
5
5
|
`&&(n&&Ie(n)&&(i+=Fe(n)),o&&(i+=Ce(o)))}return i},ot=/\r?\n/;function ee(e,t,r){return String(e).normalize().split(ot).map(i=>at(i,t,r)).join(`
|
|
@@ -12,9 +12,9 @@ import{styleText as u}from"node:util";import L,{stdin as Ue,stdout as ye}from"no
|
|
|
12
12
|
`).length-1;this.output.write(V.move(-999,t*-1))}render(){const t=ee(this._render(this)??"",ie(this.output),{hard:!0,trim:!1});if(t!==this._prevFrame){if(this.state==="initial")this.output.write(V.hide);else{const r=ut(this._prevFrame,t),i=Te(this.output);if(this.restoreCursor(),r){const n=Math.max(0,r.numLinesAfter-i),o=Math.max(0,r.numLinesBefore-i);let l=r.lines.find(a=>a>=n);if(l===void 0){this._prevFrame=t;return}if(r.lines.length===1){this.output.write(V.move(0,l-o)),this.output.write(de.lines(1));const a=t.split(`
|
|
13
13
|
`);this.output.write(a[l]),this._prevFrame=t,this.output.write(V.move(0,a.length-l-1));return}else if(r.lines.length>1){if(n<o)l=n;else{const s=l-o;s>0&&this.output.write(V.move(0,s))}this.output.write(de.down());const a=t.split(`
|
|
14
14
|
`).slice(l);this.output.write(a.join(`
|
|
15
|
-
`)),this._prevFrame=t;return}}this.output.write(de.down())}this.output.write(t),this.state==="initial"&&(this.state="active"),this._prevFrame=t}}},mt=class extends Re{options;cursor=0;get _selectedValue(){return this.options[this.cursor]}changeValue(){const t=this._selectedValue;this.value=t===void 0?void 0:t.value}constructor(t){super(t,!1),this.options=t.options;const r=this.options.findIndex(({value:n})=>n===t.initialValue),i=r===-1?0:r;this.cursor=this.options[i]?.disabled?te(i,1,this.options):i,this.changeValue(),this.on("cursor",n=>{switch(n){case"left":case"up":this.cursor=te(this.cursor,-1,this.options);break;case"down":case"right":this.cursor=te(this.cursor,1,this.options);break}this.changeValue()})}};class dt extends Re{get userInputWithCursor(){if(this.state==="submit")return this.userInput;const t=this.userInput;if(this.cursor>=t.length)return`${this.userInput}\u2588`;const r=t.slice(0,this.cursor),i=t.slice(this.cursor,this.cursor+1),n=t.slice(this.cursor+1);return`${r}${u("inverse",i)}${n}`}get cursor(){return this._cursor}constructor(t){super({...t,initialUserInput:t.initialUserInput??t.initialValue}),this.on("userInput",r=>{this._setValue(r)}),this.on("finalize",()=>{this.value||(this.value=t.defaultValue),this.value===void 0&&(this.value="")})}}function pt(){return
|
|
16
|
-
`);return r&&n.push(u("cyan",G)),n}const
|
|
17
|
-
`);_.push(I),F+=I.length}if(F>d){let v=0,C=0,$=F;const I=e-j;let R=d;const D=()=>
|
|
15
|
+
`)),this._prevFrame=t;return}}this.output.write(de.down())}this.output.write(t),this.state==="initial"&&(this.state="active"),this._prevFrame=t}}},mt=class extends Re{options;cursor=0;get _selectedValue(){return this.options[this.cursor]}changeValue(){const t=this._selectedValue;this.value=t===void 0?void 0:t.value}constructor(t){super(t,!1),this.options=t.options;const r=this.options.findIndex(({value:n})=>n===t.initialValue),i=r===-1?0:r;this.cursor=this.options[i]?.disabled?te(i,1,this.options):i,this.changeValue(),this.on("cursor",n=>{switch(n){case"left":case"up":this.cursor=te(this.cursor,-1,this.options);break;case"down":case"right":this.cursor=te(this.cursor,1,this.options);break}this.changeValue()})}};class dt extends Re{get userInputWithCursor(){if(this.state==="submit")return this.userInput;const t=this.userInput;if(this.cursor>=t.length)return`${this.userInput}\u2588`;const r=t.slice(0,this.cursor),i=t.slice(this.cursor,this.cursor+1),n=t.slice(this.cursor+1);return`${r}${u("inverse",i)}${n}`}get cursor(){return this._cursor}constructor(t){super({...t,initialUserInput:t.initialUserInput??t.initialValue}),this.on("userInput",r=>{this._setValue(r)}),this.on("finalize",()=>{this.value||(this.value=t.defaultValue),this.value===void 0&&(this.value="")})}}function pt(){return B.platform!=="win32"?B.env.TERM!=="linux":!!B.env.CI||!!B.env.WT_SESSION||!!B.env.TERMINUS_SUBLIME||B.env.ConEmuTask==="{cmd::Cmder}"||B.env.TERM_PROGRAM==="Terminus-Sublime"||B.env.TERM_PROGRAM==="vscode"||B.env.TERM==="xterm-256color"||B.env.TERM==="alacritty"||B.env.TERMINAL_EMULATOR==="JetBrains-JediTerm"}const ft=pt(),L=(e,t)=>ft?e:t,je=L("\u25C6","*"),gt=L("\u25A0","x"),bt=L("\u25B2","x"),Ae=L("\u25C7","o"),E=L("\u2502","|"),G=L("\u2514","\u2014"),yt=L("\u25CF",">"),Oe=L("\u25CB"," "),$t=L("\u25CF","\u2022"),wt=L("\u25C6","*"),xt=L("\u25B2","!"),vt=L("\u25A0","x"),Le=e=>{switch(e){case"initial":case"active":return u("cyan",je);case"cancel":return u("red",gt);case"error":return u("yellow",bt);case"submit":return u("green",Ae);case"validating":return u("dim",je)}},St=e=>{switch(e){case"initial":case"active":return u("cyan",E);case"cancel":return u("red",E);case"error":return u("yellow",E);case"submit":return u("green",E)}};function Et(e,t,r){const i=r?`${u("cyan",E)} `:"",n=se(e,t.join(" \u2022 "),i).split(`
|
|
16
|
+
`);return r&&n.push(u("cyan",G)),n}const Pe=(e,t,r,i,n,o=!1)=>{let l=t,a=0;if(o)for(let s=i-1;s>=r;s--){const c=e[s];if(c&&(l-=c.length),a++,l<=n)break}else for(let s=r;s<i;s++){const c=e[s];if(c&&(l-=c.length),a++,l<=n)break}return{lineCount:l,removals:a}},_t=({cursor:e,options:t,style:r,output:i=process.stdout,maxItems:n=Number.POSITIVE_INFINITY,columnPadding:o=0,rowPadding:l=4})=>{const a=ie(i)-o,s=Te(i),c=u("dim","..."),d=Math.max(s-l,0),h=Math.max(Math.min(n,d),5);let m=0;e>=h-3&&(m=Math.max(Math.min(e-h+3,t.length-h),0));let f=h<t.length&&m>0,g=h<t.length&&m+h<t.length;const w=Math.min(m+h,t.length),_=[];let F=0;f&&F++,g&&F++;const j=m+(f?1:0),M=w-(g?1:0);for(let v=j;v<M;v++){const C=t[v],$=C!==void 0?r(C,v===e):"",I=ee($,a,{hard:!0,trim:!1}).split(`
|
|
17
|
+
`);_.push(I),F+=I.length}if(F>d){let v=0,C=0,$=F;const I=e-j;let R=d;const D=()=>Pe(_,$,0,I,R),A=()=>Pe(_,$,I+1,_.length,R,!0);f?({lineCount:$,removals:v}=D(),$>R&&(g||(R-=1),{lineCount:$,removals:C}=A())):(g||(R-=1),{lineCount:$,removals:C}=A(),$>R&&(R-=1,{lineCount:$,removals:v}=D())),v>0&&(f=!0,_.splice(0,v)),C>0&&(g=!0,_.splice(_.length-C,C))}const O=[];f&&O.push(c);for(const v of _)for(const C of v)O.push(C);return g&&O.push(c),O};`${u("dim","\u2191/\u2193")}`,`${u("dim","Space:")}`,`${u("dim","Enter:")}`;const N={message:(e=[],{symbol:t=u("gray",E),secondarySymbol:r=u("gray",E),output:i=process.stdout,spacing:n=1,withGuide:o}={})=>{const l=[],a=o??U.withGuide,s=a?r:"",c=a?`${t} `:"",d=a?`${r} `:"";for(let m=0;m<n;m++)l.push(s);const h=Array.isArray(e)?e:e.split(`
|
|
18
18
|
`);if(h.length>0){const[m,...f]=h;m.length>0?l.push(`${c}${m}`):l.push(a?t:"");for(const g of f)g.length>0?l.push(`${d}${g}`):l.push(a?r:"")}i.write(`${l.join(`
|
|
19
19
|
`)}
|
|
20
20
|
`)},info:(e,t)=>{N.message(e,{...t,symbol:u("blue",$t)})},success:(e,t)=>{N.message(e,{...t,symbol:u("green",wt)})},step:(e,t)=>{N.message(e,{...t,symbol:u("green",Ae)})},warn:(e,t)=>{N.message(e,{...t,symbol:u("yellow",xt)})},warning:(e,t)=>{N.warn(e,t)},error:(e,t)=>{N.message(e,{...t,symbol:u("red",vt)})}},It=(e="",t)=>{const r=process.stdout,i=`${u("gray",G)} `;r.write(`${i}${u("red",e)}
|
|
@@ -25,7 +25,7 @@ ${u("gray",G)} `;r.write(`${i}${e}
|
|
|
25
25
|
`)},Ct=[`${u("dim","\u2191/\u2193")} to navigate`,`${u("dim","Enter:")} confirm`],ne=(e,t)=>e.includes(`
|
|
26
26
|
`)?e.split(`
|
|
27
27
|
`).map(r=>t(r)).join(`
|
|
28
|
-
`):t(e),fe=e=>{const t=(i,n)=>{if(i===void 0)return"";const o=i.label??String(i.value);switch(n){case"disabled":return`${u("gray",Oe)} ${ne(o,l=>u("gray",l))}${i.hint?` ${u("dim",`(${i.hint??"disabled"})`)}`:""}`;case"selected":return`${ne(o,l=>u("dim",l))}`;case"active":return`${u("green",yt)} ${o}${i.hint?` ${u("dim",`(${i.hint})`)}`:""}`;case"cancelled":return`${ne(o,l=>u(["strikethrough","dim"],l))}`;default:return`${u("dim",Oe)} ${ne(o,l=>u("dim",l))}`}},r=e.showInstructions??!0;return new mt({options:e.options,signal:e.signal,input:e.input,output:e.output,initialValue:e.initialValue,render(){const i=e.withGuide??U.withGuide,n=`${
|
|
28
|
+
`):t(e),fe=e=>{const t=(i,n)=>{if(i===void 0)return"";const o=i.label??String(i.value);switch(n){case"disabled":return`${u("gray",Oe)} ${ne(o,l=>u("gray",l))}${i.hint?` ${u("dim",`(${i.hint??"disabled"})`)}`:""}`;case"selected":return`${ne(o,l=>u("dim",l))}`;case"active":return`${u("green",yt)} ${o}${i.hint?` ${u("dim",`(${i.hint})`)}`:""}`;case"cancelled":return`${ne(o,l=>u(["strikethrough","dim"],l))}`;default:return`${u("dim",Oe)} ${ne(o,l=>u("dim",l))}`}},r=e.showInstructions??!0;return new mt({options:e.options,signal:e.signal,input:e.input,output:e.output,initialValue:e.initialValue,render(){const i=e.withGuide??U.withGuide,n=`${Le(this.state)} `,o=`${St(this.state)} `,l=se(e.output,e.message,o,n),a=`${i?`${u("gray",E)}
|
|
29
29
|
`:""}${l}
|
|
30
30
|
`;switch(this.state){case"submit":{const s=i?`${u("gray",E)} `:"",c=se(e.output,t(this.options[this.cursor],"selected"),s);return`${a}${c}`}case"cancel":{const s=i?`${u("gray",E)} `:"",c=se(e.output,t(this.options[this.cursor],"cancelled"),s);return`${a}${c}${i?`
|
|
31
31
|
${u("gray",E)}`:""}`}default:{const s=i?`${u("cyan",E)} `:"",c=a.split(`
|
|
@@ -33,8 +33,8 @@ ${u("gray",E)}`:""}`}default:{const s=i?`${u("cyan",E)} `:"",c=a.split(`
|
|
|
33
33
|
`),m=d.length+1;return`${a}${s}${_t({output:e.output,cursor:this.cursor,options:this.options,maxItems:e.maxItems,columnPadding:s.length,rowPadding:c+m,style:(f,g)=>t(f,f.disabled?"disabled":g?"active":"inactive")}).join(`
|
|
34
34
|
${s}`)}
|
|
35
35
|
${h}
|
|
36
|
-
`}}}}).prompt()};`${u("gray",E)}`;const
|
|
37
|
-
`:""}${
|
|
36
|
+
`}}}}).prompt()};`${u("gray",E)}`;const Be=e=>new dt({validate:e.validate,placeholder:e.placeholder,defaultValue:e.defaultValue,initialValue:e.initialValue,output:e.output,signal:e.signal,input:e.input,render(){const t=e?.withGuide??U.withGuide,r=`${`${t?`${u("gray",E)}
|
|
37
|
+
`:""}${Le(this.state)} `}${e.message}
|
|
38
38
|
`,i=e.placeholder&&e.placeholder.length>0?u("inverse",e.placeholder[0])+u("dim",e.placeholder.slice(1)):u(["inverse","hidden"],"_"),n=this.userInput?this.userInputWithCursor:i,o=this.value??"";switch(this.state){case"validating":{const l=t?`${u("cyan",E)} `:"",a=t?u("cyan",G):"",s=u("dim",n),c=u("dim","Validating...");return`${r}${l}${s}
|
|
39
39
|
${a} ${c}
|
|
40
40
|
`}case"error":{const l=this.error?` ${u("yellow",this.error)}`:"",a=t?`${u("yellow",E)} `:"",s=t?u("yellow",G):"";return`${r.trim()}
|
|
@@ -43,11 +43,11 @@ ${s}${l}
|
|
|
43
43
|
`}case"submit":{const l=o?`${t?" ":""}${u("dim",o)}`:"",a=t?u("gray",E):"";return`${r}${a}${l}`}case"cancel":{const l=o?` ${u(["strikethrough","dim"],o)}`:"",a=t?u("gray",E):"";return`${r}${a}${l}${o.trim()?`
|
|
44
44
|
${a}`:""}`}default:{const l=t?`${u("cyan",E)} `:"",a=t?u("cyan",G):"";return`${r}${l}${n}
|
|
45
45
|
${a}
|
|
46
|
-
`}}}}).prompt();function ge(e){return e==null?[]:Array.isArray(e)?e:[e]}function kt(e,t,r,i){var n,o=e[t],l=~i.string.indexOf(t)?r==null||r===!0?"":String(r):typeof r=="boolean"?r:~i.boolean.indexOf(t)?r==="false"?!1:r==="true"||(e._.push((n=+r,n*0===0?n:r)),!!r):(n=+r,n*0===0?n:r);e[t]=o==null?l:Array.isArray(o)?o.concat(l):[o,l]}function Tt(e,t){e=e||[],t=t||{};var r,i,n,o,l,a={_:[]},s=0,c=0,d=0,h=e.length;const m=t.alias!==void 0,f=t.unknown!==void 0,g=t.default!==void 0;if(t.alias=t.alias||{},t.string=ge(t.string),t.boolean=ge(t.boolean),m)for(r in t.alias)for(i=t.alias[r]=ge(t.alias[r]),s=0;s<i.length;s++)(t.alias[i[s]]=i.concat(r)).splice(s,1);for(s=t.boolean.length;s-- >0;)for(i=t.alias[t.boolean[s]]||[],c=i.length;c-- >0;)t.boolean.push(i[c]);for(s=t.string.length;s-- >0;)for(i=t.alias[t.string[s]]||[],c=i.length;c-- >0;)t.string.push(i[c]);if(g){for(r in t.default)if(o=typeof t.default[r],i=t.alias[r]=t.alias[r]||[],t[o]!==void 0)for(t[o].push(r),s=0;s<i.length;s++)t[o].push(i[s])}const w=f?Object.keys(t.alias):[];for(s=0;s<h;s++){if(n=e[s],n==="--"){a._=a._.concat(e.slice(++s));break}for(c=0;c<n.length&&n.charCodeAt(c)===45;c++);if(c===0)a._.push(n);else if(n.substring(c,c+3)==="no-"){if(o=n.substring(c+3),f&&!~w.indexOf(o))return t.unknown(n);a[o]=!1}else{for(d=c+1;d<n.length&&n.charCodeAt(d)!==61;d++);for(o=n.substring(c,d),l=n.substring(++d)||s+1===h||(""+e[s+1]).charCodeAt(0)===45||e[++s],i=c===2?[o]:o,d=0;d<i.length;d++){if(o=i[d],f&&!~w.indexOf(o))return t.unknown("-".repeat(c)+o);kt(a,o,d+1<i.length||l,t)}}}if(g)for(r in t.default)a[r]===void 0&&(a[r]=t.default[r]);if(m)for(r in a)for(i=t.alias[r]||[];i.length>0;)a[i.shift()]=a[r];return a}function Rt(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var ae={exports:{}},Me;function jt(){if(Me)return ae.exports;Me=1;let e=process||{},t=e.argv||[],r=e.env||{},i=!(r.NO_COLOR||t.includes("--no-color"))&&(!!r.FORCE_COLOR||t.includes("--color")||e.platform==="win32"||(e.stdout||{}).isTTY&&r.TERM!=="dumb"||!!r.CI),n=(a,s,c=a)=>d=>{let h=""+d,m=h.indexOf(s,a.length);return~m?a+o(h,s,c,m)+s:a+h+s},o=(a,s,c,d)=>{let h="",m=0;do h+=a.substring(m,d)+c,m=d+s.length,d=a.indexOf(s,m);while(~d);return h+a.substring(m)},l=(a=i)=>{let s=a?n:()=>String;return{isColorSupported:a,reset:s("\x1B[0m","\x1B[0m"),bold:s("\x1B[1m","\x1B[22m","\x1B[22m\x1B[1m"),dim:s("\x1B[2m","\x1B[22m","\x1B[22m\x1B[2m"),italic:s("\x1B[3m","\x1B[23m"),underline:s("\x1B[4m","\x1B[24m"),inverse:s("\x1B[7m","\x1B[27m"),hidden:s("\x1B[8m","\x1B[28m"),strikethrough:s("\x1B[9m","\x1B[29m"),black:s("\x1B[30m","\x1B[39m"),red:s("\x1B[31m","\x1B[39m"),green:s("\x1B[32m","\x1B[39m"),yellow:s("\x1B[33m","\x1B[39m"),blue:s("\x1B[34m","\x1B[39m"),magenta:s("\x1B[35m","\x1B[39m"),cyan:s("\x1B[36m","\x1B[39m"),white:s("\x1B[37m","\x1B[39m"),gray:s("\x1B[90m","\x1B[39m"),bgBlack:s("\x1B[40m","\x1B[49m"),bgRed:s("\x1B[41m","\x1B[49m"),bgGreen:s("\x1B[42m","\x1B[49m"),bgYellow:s("\x1B[43m","\x1B[49m"),bgBlue:s("\x1B[44m","\x1B[49m"),bgMagenta:s("\x1B[45m","\x1B[49m"),bgCyan:s("\x1B[46m","\x1B[49m"),bgWhite:s("\x1B[47m","\x1B[49m"),blackBright:s("\x1B[90m","\x1B[39m"),redBright:s("\x1B[91m","\x1B[39m"),greenBright:s("\x1B[92m","\x1B[39m"),yellowBright:s("\x1B[93m","\x1B[39m"),blueBright:s("\x1B[94m","\x1B[39m"),magentaBright:s("\x1B[95m","\x1B[39m"),cyanBright:s("\x1B[96m","\x1B[39m"),whiteBright:s("\x1B[97m","\x1B[39m"),bgBlackBright:s("\x1B[100m","\x1B[49m"),bgRedBright:s("\x1B[101m","\x1B[49m"),bgGreenBright:s("\x1B[102m","\x1B[49m"),bgYellowBright:s("\x1B[103m","\x1B[49m"),bgBlueBright:s("\x1B[104m","\x1B[49m"),bgMagentaBright:s("\x1B[105m","\x1B[49m"),bgCyanBright:s("\x1B[106m","\x1B[49m"),bgWhiteBright:s("\x1B[107m","\x1B[49m")}};return ae.exports=l(),ae.exports.createColors=l,ae.exports}var At=jt();const Ot=Rt(At),{cyan:
|
|
46
|
+
`}}}}).prompt();function ge(e){return e==null?[]:Array.isArray(e)?e:[e]}function kt(e,t,r,i){var n,o=e[t],l=~i.string.indexOf(t)?r==null||r===!0?"":String(r):typeof r=="boolean"?r:~i.boolean.indexOf(t)?r==="false"?!1:r==="true"||(e._.push((n=+r,n*0===0?n:r)),!!r):(n=+r,n*0===0?n:r);e[t]=o==null?l:Array.isArray(o)?o.concat(l):[o,l]}function Tt(e,t){e=e||[],t=t||{};var r,i,n,o,l,a={_:[]},s=0,c=0,d=0,h=e.length;const m=t.alias!==void 0,f=t.unknown!==void 0,g=t.default!==void 0;if(t.alias=t.alias||{},t.string=ge(t.string),t.boolean=ge(t.boolean),m)for(r in t.alias)for(i=t.alias[r]=ge(t.alias[r]),s=0;s<i.length;s++)(t.alias[i[s]]=i.concat(r)).splice(s,1);for(s=t.boolean.length;s-- >0;)for(i=t.alias[t.boolean[s]]||[],c=i.length;c-- >0;)t.boolean.push(i[c]);for(s=t.string.length;s-- >0;)for(i=t.alias[t.string[s]]||[],c=i.length;c-- >0;)t.string.push(i[c]);if(g){for(r in t.default)if(o=typeof t.default[r],i=t.alias[r]=t.alias[r]||[],t[o]!==void 0)for(t[o].push(r),s=0;s<i.length;s++)t[o].push(i[s])}const w=f?Object.keys(t.alias):[];for(s=0;s<h;s++){if(n=e[s],n==="--"){a._=a._.concat(e.slice(++s));break}for(c=0;c<n.length&&n.charCodeAt(c)===45;c++);if(c===0)a._.push(n);else if(n.substring(c,c+3)==="no-"){if(o=n.substring(c+3),f&&!~w.indexOf(o))return t.unknown(n);a[o]=!1}else{for(d=c+1;d<n.length&&n.charCodeAt(d)!==61;d++);for(o=n.substring(c,d),l=n.substring(++d)||s+1===h||(""+e[s+1]).charCodeAt(0)===45||e[++s],i=c===2?[o]:o,d=0;d<i.length;d++){if(o=i[d],f&&!~w.indexOf(o))return t.unknown("-".repeat(c)+o);kt(a,o,d+1<i.length||l,t)}}}if(g)for(r in t.default)a[r]===void 0&&(a[r]=t.default[r]);if(m)for(r in a)for(i=t.alias[r]||[];i.length>0;)a[i.shift()]=a[r];return a}function Rt(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var ae={exports:{}},Me;function jt(){if(Me)return ae.exports;Me=1;let e=process||{},t=e.argv||[],r=e.env||{},i=!(r.NO_COLOR||t.includes("--no-color"))&&(!!r.FORCE_COLOR||t.includes("--color")||e.platform==="win32"||(e.stdout||{}).isTTY&&r.TERM!=="dumb"||!!r.CI),n=(a,s,c=a)=>d=>{let h=""+d,m=h.indexOf(s,a.length);return~m?a+o(h,s,c,m)+s:a+h+s},o=(a,s,c,d)=>{let h="",m=0;do h+=a.substring(m,d)+c,m=d+s.length,d=a.indexOf(s,m);while(~d);return h+a.substring(m)},l=(a=i)=>{let s=a?n:()=>String;return{isColorSupported:a,reset:s("\x1B[0m","\x1B[0m"),bold:s("\x1B[1m","\x1B[22m","\x1B[22m\x1B[1m"),dim:s("\x1B[2m","\x1B[22m","\x1B[22m\x1B[2m"),italic:s("\x1B[3m","\x1B[23m"),underline:s("\x1B[4m","\x1B[24m"),inverse:s("\x1B[7m","\x1B[27m"),hidden:s("\x1B[8m","\x1B[28m"),strikethrough:s("\x1B[9m","\x1B[29m"),black:s("\x1B[30m","\x1B[39m"),red:s("\x1B[31m","\x1B[39m"),green:s("\x1B[32m","\x1B[39m"),yellow:s("\x1B[33m","\x1B[39m"),blue:s("\x1B[34m","\x1B[39m"),magenta:s("\x1B[35m","\x1B[39m"),cyan:s("\x1B[36m","\x1B[39m"),white:s("\x1B[37m","\x1B[39m"),gray:s("\x1B[90m","\x1B[39m"),bgBlack:s("\x1B[40m","\x1B[49m"),bgRed:s("\x1B[41m","\x1B[49m"),bgGreen:s("\x1B[42m","\x1B[49m"),bgYellow:s("\x1B[43m","\x1B[49m"),bgBlue:s("\x1B[44m","\x1B[49m"),bgMagenta:s("\x1B[45m","\x1B[49m"),bgCyan:s("\x1B[46m","\x1B[49m"),bgWhite:s("\x1B[47m","\x1B[49m"),blackBright:s("\x1B[90m","\x1B[39m"),redBright:s("\x1B[91m","\x1B[39m"),greenBright:s("\x1B[92m","\x1B[39m"),yellowBright:s("\x1B[93m","\x1B[39m"),blueBright:s("\x1B[94m","\x1B[39m"),magentaBright:s("\x1B[95m","\x1B[39m"),cyanBright:s("\x1B[96m","\x1B[39m"),whiteBright:s("\x1B[97m","\x1B[39m"),bgBlackBright:s("\x1B[100m","\x1B[49m"),bgRedBright:s("\x1B[101m","\x1B[49m"),bgGreenBright:s("\x1B[102m","\x1B[49m"),bgYellowBright:s("\x1B[103m","\x1B[49m"),bgBlueBright:s("\x1B[104m","\x1B[49m"),bgMagentaBright:s("\x1B[105m","\x1B[49m"),bgCyanBright:s("\x1B[106m","\x1B[49m"),bgWhiteBright:s("\x1B[107m","\x1B[49m")}};return ae.exports=l(),ae.exports.createColors=l,ae.exports}var At=jt();const Ot=Rt(At),{cyan:Lt,magenta:Pt,yellow:Bt}=Ot,q=[{name:"engine",display:"Engine",description:"with the PlayCanvas Engine API directly",color:Bt},{name:"react",display:"React",description:"with @playcanvas/react",color:Lt},{name:"web-components",display:"Web Components",description:"with @playcanvas/web-components",color:Pt}],Mt={physics:{engine:["assets"],react:["react"],"web-components":["assets"]}},K=[{name:"spinning-cube",display:"Spinning Cube",category:"Basics",description:"a rotating cube and a light"},{name:"model-viewer",display:"Model Viewer",category:"Viewers",description:"a glTF model, orbit controls and a procedural sky"},{name:"splat-viewer",display:"Splat Viewer",category:"Viewers",description:"a Gaussian splat and orbit controls"},{name:"product-configurator",display:"Product Configurator",category:"Viewers",description:"product variants, materials and orbit controls"},{name:"first-person-controller",display:"First-Person Controller",category:"Games",description:"first-person movement, mouse look and jumping",features:["physics"]},{name:"third-person-controller",display:"Third-Person Controller",category:"Games",description:"an animated character, follow camera and locomotion",features:["physics"]},{name:"sprite-game",display:"Sprite Game",category:"Games",description:"an orthographic camera, sprite animation and input"},{name:"web-lens",display:"Snapchat Web Lens",category:"Games",description:"a portrait game with Snapchat leaderboards, packaged for upload",omit:["src/starter.css","src/assets"],outro:`To publish it to Snapchat, run:
|
|
47
47
|
|
|
48
48
|
npm run package
|
|
49
49
|
|
|
50
|
-
and upload the zip it writes to dist/ at https://my-lenses.snapchat.com/`},{name:"scene-editor",display:"Scene Editor",category:"Tools",description:"
|
|
50
|
+
and upload the zip it writes to dist/ at https://my-lenses.snapchat.com/`},{name:"scene-editor",display:"Scene Editor",category:"Tools",description:"click to select, gizmos and a scene saved in the browser"},{name:"vr-starter",display:"VR Starter",category:"XR",description:"immersive VR, controllers and locomotion"},{name:"ar-placement",display:"AR Placement",category:"XR",description:"mobile AR, hit testing and object placement"}],Nt=async({argFormat:e,yes:t,cancel:r})=>{if(e&&q.some(n=>n.name===e))return e;if(e&&t)throw new Error(`Unknown format: ${e}. Choose from: ${q.map(n=>n.name).join(", ")}`);if(t)return q[0].name;const i=await fe({message:e?`"${e}" isn't a valid format. Please choose from below: `:"Select a format:",options:q.map(n=>({label:n.color(n.display),value:n.name,hint:n.description}))});return Y(i)&&r(),i},Dt=async({argStarter:e,yes:t,cancel:r})=>{if(e&&K.some(n=>n.name===e))return e;if(t){if(!e)return K[0].name;throw new Error(`Unknown starter: ${e}. Choose from: ${K.map(n=>n.name).join(", ")}`)}const i=await fe({message:e?`"${e}" isn't a valid starter. Please choose from below: `:"Select a starter:",initialValue:K[0].name,options:K.map(n=>({label:n.display,value:n.name,hint:`${n.category} \xB7 ${n.description}`}))});return Y(i)&&r(),i},Ne=e=>/^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(e),be=e=>e.trim().toLowerCase().replace(/\s+/g,"-").replace(/^[._]/,"").replace(/[^a-z\d\-~]+/g,"-"),Wt=async({targetDir:e,yes:t,cancel:r})=>{let i=b.basename(b.resolve(e));if(!Ne(i)){if(t)return be(i);const n=await Be({message:"Package name:",defaultValue:be(i),placeholder:be(i),validate:o=>{if(!o||!Ne(o))return"Invalid package.json name"}});if(Y(n))return r();i=n}return i},oe=e=>e.trim().replace(/\/+$/g,""),Vt=e=>{const t=y.readdirSync(e);return t.length===0||t.length===1&&t[0]===".git"},Ut=e=>{if(y.existsSync(e))for(const t of y.readdirSync(e))t!==".git"&&y.rmSync(b.resolve(e,t),{recursive:!0,force:!0})},Gt=async({argTargetDir:e,defaultTargetDir:t,yes:r,cancel:i})=>{let n=e?oe(String(e)):void 0;if(!n&&r)return t;if(!n){const o=await Be({message:"Project name:",defaultValue:t,placeholder:t,validate:l=>{if(l&&!oe(l))return"Invalid project name"}});Y(o)&&i(),n=oe(o)}return n},Kt=async({targetDir:e,argOverwrite:t,argKeep:r,yes:i,cancel:n})=>{if(t&&r)throw new Error("Pass --keep or --overwrite, not both.");if(!y.existsSync(e)||Vt(e))return!1;let o="no";if(t)o="yes";else if(r)o="keep";else{if(i)throw new Error(`Target directory "${e}" is not empty. Pass --keep to add the project to its files, or --overwrite to remove them.`);o=await fe({message:(e==="."?"Current directory":`Target directory "${e}"`)+" is not empty. Please choose how to proceed:",options:[{label:"Cancel operation",value:"no"},{label:"Remove existing files and continue",value:"yes"},{label:"Keep existing files and continue",value:"keep"}]})}if(Y(o))return n();switch(o){case"yes":return Ut(e),!1;case"keep":if(y.existsSync(b.join(e,"package.json")))throw new Error(`Target directory "${e}" already has a package.json, so it can't keep its files. Pass --overwrite to remove them.`);return!0;default:return n()}},Ht=["scripts","dependencies","devDependencies","peerDependencies","engines"],De={"README.md":"## Prerequisites","AGENTS.md":"## Checking your work"},zt="## Agent skills",Jt=["npm","pnpm","yarn","bun"],Yt=e=>e==="yarn"?{install:"yarn",run:"yarn"}:{install:`${e} install`,run:`${e} run`},qt=e=>{const{cwd:t,targetDir:r,format:i,starter:n,packageName:o,skills:l,keep:a,renameFiles:s}=e,c=b.resolve(t,r);y.mkdirSync(c,{recursive:!0});const d=process.env.npm_config_user_agent?.split("/")[0]??"npm",h=Jt.includes(d)?d:"npm",{install:m,run:f}=Yt(h),g=p=>h==="npm"?p:p.replace(/\bnpm run ([^\s`]+) -- /g,`${f} $1 `).replace(/\bnpm run\b/g,f).replace(/\bnpm install\b/g,m);N.step(`Creating project in ${c}...`);const w=He(import.meta.url),_=b.dirname(w),F=e.templatesDir??[b.resolve(_,"../templates"),b.resolve(_,"../../templates")].find(p=>y.existsSync(b.join(p,i)));if(!F)throw new Error(`Format directory not found for: ${i}`);const j=K.find(p=>p.name===n);if(!j)throw new Error(`Starter not found: ${n}`);const M=b.join(F,"_common"),O=b.join(F,i,"base"),v=b.join(F,"_shared",n),C=b.join(F,i,n);if(!y.existsSync(C))throw new Error(`Starter directory not found: ${i}/${n}`);const $=a?y.mkdtempSync(b.join(Ke.tmpdir(),"create-playcanvas-")):c,I=[M,O,v,...(j.features??[]).flatMap(p=>Mt[p][i].map(k=>b.join(F,"_features",p,k))),C].filter(p=>y.existsSync(p)),R=j.omit??[],D=R.flatMap(p=>[M,O].map(k=>b.join(k,p))),A=Object.keys(De).map(p=>b.join(v,p));for(const p of I){const k=b.join(p,"package.json");y.cpSync(p,$,{recursive:!0,filter:x=>x!==k&&!A.includes(x)&&!D.includes(x)})}for(const[p,k]of Object.entries(De)){const x=b.join($,p);if(!y.existsSync(x))continue;let T=y.readFileSync(x,"utf8");const Q=b.join(v,p);if(y.existsSync(Q)){const J=`${y.readFileSync(Q,"utf8").trimEnd()}
|
|
51
51
|
|
|
52
52
|
${k}`;T=T.replace(k,()=>J)}l||(T=`${T.split(/^(?=## )/m).filter(J=>!J.startsWith(zt)).join("").trimEnd()}
|
|
53
53
|
`),T=T.split(`
|
|
@@ -59,7 +59,7 @@ ${k}`;T=T.replace(k,()=>J)}l||(T=`${T.split(/^(?=## )/m).filter(J=>!J.startsWith
|
|
|
59
59
|
${m}`,z+=`
|
|
60
60
|
${f} dev`,j.outro&&(z+=`
|
|
61
61
|
|
|
62
|
-
${g(j.outro)}`),Ft(z)},
|
|
62
|
+
${g(j.outro)}`),Ft(z)},P=Tt(process.argv.slice(2),{alias:{b:"boilerplate",f:"format",h:"help",s:"starter",t:"template",y:"yes"},boolean:["help","overwrite","keep","yes"],string:["format","starter","template","boilerplate"]}),Xt=process.cwd(),We=e=>e.map((t,r)=>` ${t.name.padEnd(27)}${t.description}${r?"":" (default)"}`).join(`
|
|
63
63
|
`),Qt=`Usage: npm create playcanvas@latest [DIRECTORY] -- [OPTION]...
|
|
64
64
|
|
|
65
65
|
Create a new PlayCanvas project with TypeScript in DIRECTORY.
|
|
@@ -83,4 +83,4 @@ ${We(K)}
|
|
|
83
83
|
|
|
84
84
|
Without a terminal, as when an AI agent or CI runs it, the CLI takes the
|
|
85
85
|
defaults for any option not passed, as --yes does.
|
|
86
|
-
`,Zt={_gitignore:".gitignore",_npmrc:".npmrc"},er="playcanvas-project",tr=async()=>{const e=
|
|
86
|
+
`,Zt={_gitignore:".gitignore",_npmrc:".npmrc"},er="playcanvas-project",tr=async()=>{const e=P._[0]?oe(String(P._[0])):void 0,t=P.format??P.template,r=P.starter??P.boilerplate,i=P.overwrite,n=P.keep,o=!!(process.stdin.isTTY&&process.stdout.isTTY),l=P.yes||!o;if(P.help){console.log(Qt);return}!o&&!P.yes&&N.info("Not running in a terminal, so using the defaults for any option not passed");const a=()=>{It("Operation cancelled"),process.exit(0)},s=await Gt({argTargetDir:e,defaultTargetDir:er,yes:l,cancel:a}),c=await Kt({targetDir:s,argOverwrite:i,argKeep:n,yes:l,cancel:a}),d=await Wt({targetDir:s,yes:l,cancel:a}),h=await Nt({argFormat:t,yes:l,cancel:a}),m=await Dt({argStarter:r,yes:l,cancel:a}),f=P.skills??!0;qt({cwd:Xt,targetDir:s,format:h,starter:m,packageName:d,skills:f,keep:c,renameFiles:Zt})};tr().catch(e=>{console.error(e instanceof Error?e.message:e),process.exitCode=1});
|
package/package.json
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## Scene Editor
|
|
2
|
+
|
|
3
|
+
This project is a scene editor that saves its scene in the browser.
|
|
4
|
+
|
|
5
|
+
- `src/scene-editor.ts` is the only code that creates, selects, transforms or saves the objects. Add an editing feature there, and let the HUD reach it through the script's `mode`, `add()`, `remove()` and `reset()` and its `editor:select` app event.
|
|
6
|
+
- The scene is saved to `localStorage` after every change and loaded on start, so a saved scene hides a change to `DEFAULT_SCENE`. Press **Reset**, or clear `localStorage`, before you check one.
|
|
7
|
+
- Bump `STORAGE_KEY` whenever the `Item` type changes. A scene saved in the old format would otherwise be loaded as if it were the new one.
|
|
8
|
+
- A click only selects. The script tells a click from a drag by how far the pointer moved, and ignores one that started on a gizmo handle, so keep both checks when you change the pointer handling.
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
## Scene Editor
|
|
2
2
|
|
|
3
|
-
A small
|
|
3
|
+
A small scene editor. Select an object by clicking it, transform it with a gizmo, add and delete objects, and find the scene as you left it when you come back.
|
|
4
4
|
|
|
5
5
|
### Controls
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Click an object to select it, and click empty space to clear the selection. Drag the gizmo to transform the selected object, and choose **Move**, **Rotate** or **Scale** to change the gizmo. Drag anywhere else to orbit the camera. **Add box**, **Add sphere** and **Add cone** add an object at the origin, **Delete** removes the selected one and **Reset** brings back the default scene.
|
|
8
|
+
|
|
9
|
+
### How it fits together
|
|
10
|
+
|
|
11
|
+
- `src/scene-editor.ts` is the editor, an engine script. It owns the objects, the selection, the gizmo and the saved scene, and it fires an `editor:select` app event for the HUD to follow.
|
|
12
|
+
- `src/main.ts` (`src/App.tsx` in React) sets up the camera, lights and grid, and wires the HUD's buttons to the editor's `mode`, `add()`, `remove()` and `reset()`.
|
|
13
|
+
- The scene is saved to `localStorage` after every change, as a list of `Item`s: a shape with a position, rotation and scale.
|
|
14
|
+
|
|
15
|
+
### Make it your own
|
|
16
|
+
|
|
17
|
+
- **Edit your own objects.** `create()` builds an entity from an `Item`. Swap the primitive shapes for your own models, and add to `Item` whatever else an object has to remember, such as a color or a name. Bump `STORAGE_KEY` whenever `Item` changes, so a scene saved in the old format is ignored.
|
|
18
|
+
- **Save somewhere else.** `save()` and the load in `initialize()` are the only code that touches `localStorage`. Point them at a file or your own server to share a scene.
|
|
19
|
+
- **Keep the selection and the gizmo.** Neither depends on what the objects are. `pick()` matches the mesh under the pointer to the object that owns it, so change that match if an object becomes a hierarchy of meshes.
|
|
20
|
+
- **Add undo.** There is none yet. Every change already passes through `add()`, `remove()` or the gizmo's `transform:end`, which is where a history would record it.
|
|
@@ -1,56 +1,206 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// The editor: it owns the objects, the selection, the gizmo and the saved scene, and it is the file
|
|
2
|
+
// to grow to make this starter your own (see "Make it your own" in README.md). The entry file only
|
|
3
|
+
// sets up the camera, light and grid around it, and wires the HUD to `mode`, `add()`, `remove()`,
|
|
4
|
+
// `reset()` and the `editor:select` app event
|
|
5
|
+
import type { Layer, MeshInstance } from 'playcanvas';
|
|
6
|
+
import {
|
|
7
|
+
Color,
|
|
8
|
+
Entity,
|
|
9
|
+
Gizmo,
|
|
10
|
+
Picker,
|
|
11
|
+
RotateGizmo,
|
|
12
|
+
ScaleGizmo,
|
|
13
|
+
Script,
|
|
14
|
+
StandardMaterial,
|
|
15
|
+
TranslateGizmo
|
|
16
|
+
} from 'playcanvas';
|
|
3
17
|
|
|
4
|
-
type Mode = 'translate' | 'rotate' | 'scale';
|
|
18
|
+
export type Mode = 'translate' | 'rotate' | 'scale';
|
|
19
|
+
export type Shape = 'box' | 'sphere' | 'cone';
|
|
20
|
+
|
|
21
|
+
/** The app event the editor fires whenever the selection changes, for the HUD to follow. */
|
|
22
|
+
export type EditorEvents = {
|
|
23
|
+
'editor:select': (selected: Entity | null) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type Triple = [number, number, number];
|
|
27
|
+
|
|
28
|
+
/** One object, as it is saved. */
|
|
29
|
+
type Item = { shape: Shape; position: Triple; rotation: Triple; scale: Triple };
|
|
30
|
+
|
|
31
|
+
// bump the version whenever Item changes, so a scene saved in the old format is ignored
|
|
32
|
+
const STORAGE_KEY = 'scene-editor:v1';
|
|
33
|
+
|
|
34
|
+
const GIZMOS = { translate: TranslateGizmo, rotate: RotateGizmo, scale: ScaleGizmo };
|
|
35
|
+
const COLORS: Record<Shape, string> = { box: '#ff5145', sphere: '#24addf', cone: '#ffc033' };
|
|
36
|
+
const DEFAULT_SCENE: Item[] = [
|
|
37
|
+
{ shape: 'box', position: [-1.2, 0.5, 0], rotation: [0, 0, 0], scale: [1, 1, 1] },
|
|
38
|
+
{ shape: 'sphere', position: [1.2, 0.5, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// how far the pointer can travel, in CSS pixels, and still count as a click rather than an orbit
|
|
42
|
+
const CLICK_SLOP = 4;
|
|
5
43
|
|
|
6
44
|
class SceneEditor extends Script {
|
|
7
45
|
static scriptName = 'sceneEditor';
|
|
8
46
|
|
|
47
|
+
/** The camera the gizmo and the picker render from. Set it when the script is created. */
|
|
9
48
|
camera!: Entity;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
49
|
+
|
|
50
|
+
selected: Entity | null = null;
|
|
51
|
+
items: Entity[] = [];
|
|
52
|
+
|
|
13
53
|
gizmo?: TranslateGizmo | RotateGizmo | ScaleGizmo;
|
|
54
|
+
gizmoLayer!: Layer;
|
|
55
|
+
gizmoMode: Mode = 'translate';
|
|
56
|
+
picker!: Picker;
|
|
57
|
+
materials = {} as Record<Shape, StandardMaterial>;
|
|
58
|
+
|
|
59
|
+
// where the pointer went down, and whether it went down on one of the gizmo's handles
|
|
60
|
+
downX = 0;
|
|
61
|
+
downY = 0;
|
|
62
|
+
onHandle = false;
|
|
63
|
+
|
|
64
|
+
get mode() {
|
|
65
|
+
return this.gizmoMode;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Which gizmo the selected object shows. */
|
|
69
|
+
set mode(mode: Mode) {
|
|
70
|
+
if (mode === this.gizmoMode) return;
|
|
71
|
+
this.gizmoMode = mode;
|
|
72
|
+
|
|
73
|
+
// a mode set as the script is created arrives before initialize()
|
|
74
|
+
if (this.gizmo) this.createGizmo();
|
|
75
|
+
}
|
|
14
76
|
|
|
15
77
|
initialize() {
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
78
|
+
this.gizmoLayer = Gizmo.createLayer(this.app);
|
|
79
|
+
this.picker = new Picker(this.app, 1, 1);
|
|
80
|
+
for (const [shape, color] of Object.entries(COLORS) as [Shape, string][]) {
|
|
81
|
+
const material = new StandardMaterial();
|
|
82
|
+
material.diffuse = new Color().fromString(color);
|
|
83
|
+
material.update();
|
|
84
|
+
this.materials[shape] = material;
|
|
85
|
+
}
|
|
18
86
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
87
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
88
|
+
this.build(saved ? JSON.parse(saved) : DEFAULT_SCENE);
|
|
89
|
+
this.createGizmo();
|
|
90
|
+
|
|
91
|
+
const { canvas } = this.app.graphicsDevice;
|
|
92
|
+
canvas.addEventListener('pointerdown', this.onPointerDown);
|
|
93
|
+
canvas.addEventListener('pointerup', this.onPointerUp);
|
|
94
|
+
|
|
95
|
+
this.on('destroy', () => {
|
|
96
|
+
canvas.removeEventListener('pointerdown', this.onPointerDown);
|
|
97
|
+
canvas.removeEventListener('pointerup', this.onPointerUp);
|
|
98
|
+
this.build([]);
|
|
99
|
+
this.gizmo?.destroy();
|
|
100
|
+
this.picker.destroy();
|
|
101
|
+
this.app.scene.layers.remove(this.gizmoLayer);
|
|
102
|
+
Object.values(this.materials).forEach((material) => material.destroy());
|
|
29
103
|
});
|
|
30
|
-
this.on('destroy', () => this.gizmo?.destroy());
|
|
31
|
-
this.updateButtons();
|
|
32
104
|
}
|
|
33
105
|
|
|
34
|
-
|
|
35
|
-
|
|
106
|
+
/** Adds an object at the origin and selects it, ready to move. */
|
|
107
|
+
add(shape: Shape) {
|
|
108
|
+
this.select(this.create({ shape, position: [0, 0.5, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }));
|
|
109
|
+
this.save();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Deletes the selected object. */
|
|
113
|
+
remove() {
|
|
114
|
+
if (!this.selected) return;
|
|
115
|
+
this.items.splice(this.items.indexOf(this.selected), 1);
|
|
116
|
+
this.selected.destroy();
|
|
117
|
+
this.select(null);
|
|
118
|
+
this.save();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Forgets the saved scene and goes back to the default one. */
|
|
122
|
+
reset() {
|
|
123
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
124
|
+
this.build(DEFAULT_SCENE);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
select(item: Entity | null) {
|
|
128
|
+
this.selected = item;
|
|
129
|
+
if (item) this.gizmo?.attach(item);
|
|
130
|
+
else this.gizmo?.detach();
|
|
131
|
+
this.app.fire('editor:select', item);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// replaces every object with the ones in a saved scene
|
|
135
|
+
build(scene: Item[]) {
|
|
136
|
+
this.select(null);
|
|
137
|
+
this.items.splice(0).forEach((item) => item.destroy());
|
|
138
|
+
scene.forEach((item) => this.create(item));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
create({ shape, position, rotation, scale }: Item) {
|
|
142
|
+
const entity = new Entity(shape);
|
|
143
|
+
entity.addComponent('render', { type: shape, material: this.materials[shape] });
|
|
144
|
+
entity.setLocalPosition(...position);
|
|
145
|
+
entity.setLocalEulerAngles(...rotation);
|
|
146
|
+
entity.setLocalScale(...scale);
|
|
147
|
+
this.entity.addChild(entity);
|
|
148
|
+
this.items.push(entity);
|
|
149
|
+
return entity;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
save() {
|
|
153
|
+
const scene = this.items.map((item) => ({
|
|
154
|
+
shape: item.render!.type,
|
|
155
|
+
position: item.getLocalPosition().toArray(),
|
|
156
|
+
rotation: item.getLocalEulerAngles().toArray(),
|
|
157
|
+
scale: item.getLocalScale().toArray()
|
|
158
|
+
}));
|
|
159
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(scene));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// each gizmo is its own class, so changing mode swaps one for another on the same object
|
|
163
|
+
createGizmo() {
|
|
36
164
|
this.gizmo?.destroy();
|
|
37
|
-
|
|
38
|
-
this.gizmo
|
|
39
|
-
|
|
40
|
-
? new RotateGizmo(camera, this.layer)
|
|
41
|
-
: mode === 'scale'
|
|
42
|
-
? new ScaleGizmo(camera, this.layer)
|
|
43
|
-
: new TranslateGizmo(camera, this.layer);
|
|
44
|
-
this.gizmo.attach([this.targets[this.selected]]);
|
|
45
|
-
document.querySelectorAll<HTMLButtonElement>('[data-mode]').forEach((button) => {
|
|
46
|
-
button.ariaPressed = String(button.dataset.mode === mode);
|
|
165
|
+
this.gizmo = new GIZMOS[this.gizmoMode](this.camera.camera!, this.gizmoLayer);
|
|
166
|
+
this.gizmo.on('pointer:down', (_x: number, _y: number, handle?: MeshInstance) => {
|
|
167
|
+
this.onHandle = !!handle;
|
|
47
168
|
});
|
|
48
|
-
}
|
|
49
169
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
170
|
+
// the camera's own scripts would orbit with the same drag, so hold them until it ends
|
|
171
|
+
this.gizmo.on('transform:start', () => this.holdCamera(true));
|
|
172
|
+
this.gizmo.on('transform:end', () => {
|
|
173
|
+
this.holdCamera(false);
|
|
174
|
+
this.save();
|
|
53
175
|
});
|
|
176
|
+
if (this.selected) this.gizmo.attach(this.selected);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
holdCamera(held: boolean) {
|
|
180
|
+
if (this.camera.script) this.camera.script.enabled = !held;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
onPointerDown = (event: PointerEvent) => {
|
|
184
|
+
this.downX = event.offsetX;
|
|
185
|
+
this.downY = event.offsetY;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// a click selects what is under it, or nothing. A drag on a handle or to orbit the camera isn't one
|
|
189
|
+
onPointerUp = (event: PointerEvent) => {
|
|
190
|
+
const moved = Math.hypot(event.offsetX - this.downX, event.offsetY - this.downY);
|
|
191
|
+
if (!this.onHandle && moved <= CLICK_SLOP) this.pick(event.offsetX, event.offsetY);
|
|
192
|
+
this.onHandle = false;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// the picker renders the scene's meshes into an offscreen buffer and reads back the one under
|
|
196
|
+
// the pointer, so it is exact for any mesh, and it sees only the World layer, not the gizmo
|
|
197
|
+
async pick(x: number, y: number) {
|
|
198
|
+
const { canvas } = this.app.graphicsDevice;
|
|
199
|
+
const world = this.app.scene.layers.getLayerByName('World')!;
|
|
200
|
+
this.picker.resize(canvas.clientWidth, canvas.clientHeight);
|
|
201
|
+
this.picker.prepare(this.camera.camera!, this.app.scene, [world]);
|
|
202
|
+
const [hit] = await this.picker.getSelectionAsync(x, y);
|
|
203
|
+
this.select(this.items.find((item) => item.render!.meshInstances.includes(hit as MeshInstance)) ?? null);
|
|
54
204
|
}
|
|
55
205
|
}
|
|
56
206
|
|