create-playcanvas 0.7.0 → 0.8.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/dist/index.mjs +35 -33
- package/package.json +4 -4
- package/skills/add-effects/SKILL.md +17 -6
- package/skills/apply-conventions/SKILL.md +2 -2
- package/skills/bake-lighting/SKILL.md +19 -13
- package/skills/build-app/SKILL.md +7 -5
- package/skills/light-scene/SKILL.md +38 -23
- package/skills/override-shader-chunks/SKILL.md +7 -1
- package/skills/reduce-draw-calls/SKILL.md +6 -0
- package/skills/reuse-scripts/SKILL.md +21 -17
- package/skills/reuse-scripts/references/direct-engine.md +5 -25
- package/skills/reuse-scripts/references/react.md +2 -12
- package/skills/reuse-scripts/references/web-components.md +2 -15
- package/skills/verify-pixels/SKILL.md +41 -21
- package/templates/engine/base/package.json +3 -3
- package/templates/react/ar-placement/src/App.tsx +29 -21
- package/templates/react/base/package.json +9 -9
- package/templates/react/sprite-game/src/App.tsx +21 -13
- package/templates/web-components/base/package.json +4 -4
- package/templates/web-components/interactive-sphere/index.html +1 -1
- package/templates/web-components/splat-viewer/index.html +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,55 +1,57 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{styleText as
|
|
3
|
-
`);let d=!1;for(let h=0;h<
|
|
4
|
-
`){c&&(i+=we(""));const p=n
|
|
5
|
-
`&&(n
|
|
6
|
-
`)}function
|
|
2
|
+
import{styleText as l}from"node:util";import B,{stdin as Le,stdout as he}from"node:process";import Ne from"node:readline";import"node:tty";import $ from"node:fs";import y from"node:path";import{fileURLToPath as De}from"node:url";const We=(()=>{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}})(),Ve=e=>e===12288||e>=65281&&e<=65376||e>=65504&&e<=65510,Ue=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,Ge=/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]|\u001b\]8;[^;]*;.*?(?:\u0007|\u001b\u005c)/y,Ke=/[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y,me=/(?:(?![\uFF61-\uFF9F\uFF00-\uFFEF])[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Tangut}]){1,1000}/yu,He=/\t{1,1000}/y,de=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,Je=new RegExp("\\p{M}+","gu"),Ye={limit:1/0,ellipsis:""},fe=(e,t={},r={})=>{const i=t.limit??1/0,n=t.ellipsis??"",c=t?.ellipsisWidth??(n?fe(n,Ye,r).width:0),a=0,o=r.controlWidth??0,s=r.tabWidth??8,u=r.emojiWidth??2,d=2,h=r.regularWidth??1,m=r.wideWidth??d,p=[[ze,h],[Ge,a],[Ke,o],[He,s],[de,u],[me,m]];let f=0,b=0,F=e.length,T=0,O=!1,M=F,k=Math.max(0,i-c),w=0,S=0,x=0,E=0;e:for(;;){if(S>w||b>=F&&b>f){const _=e.slice(w,S)||e.slice(f,b);T=0;for(const R of _.replaceAll(Je,"")){const I=R.codePointAt(0)||0;if(Ve(I)?E=d:Ue(I)?E=m:E=h,x+E>k&&(M=Math.min(M,Math.max(w,f)+T)),x+E>i){O=!0;break e}T+=R.length,x+=E}w=S=0}if(b>=F)break e;for(let _=0,R=p.length;_<R;_++){const[I,g]=p[_];if(I.lastIndex=b,I.test(e)){if(T=I===me?We(e.slice(b,I.lastIndex)):I===de?1:I.lastIndex-b,E=T*g,x+E>k&&(M=Math.min(M,b+Math.floor((k-x)/g))),x+E>i){O=!0;break e}x+=E,w=f,S=b,b=f=I.lastIndex;continue e}}b+=1}return{width:O?k:x,index:O?M:F,truncated:O,ellipsed:O&&i>=c}},qe={limit:1/0,ellipsis:"",ellipsisWidth:0},N=(e,t={})=>fe(e,qe,t).width,z="\x1B",pe="\x9B",Xe=39,te="\x07",ge="[",Ze="]",be="m",re=`${Ze}8;;`,ye=new RegExp(`(?:\\${ge}(?<code>\\d+)m|\\${re}(?<uri>.*)${te})`,"y"),$e=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},xe=e=>`${z}${ge}${e}${be}`,we=e=>`${z}${re}${e}${te}`,se=(e,t,r)=>{const i=t[Symbol.iterator]();let n=!1,c=!1,a=e.at(-1),o=a===void 0?0:N(a),s=i.next(),u=i.next(),d=0;for(;!s.done;){const h=s.value,m=N(h);o+m<=r?e[e.length-1]+=h:(e.push(h),o=0),(h===z||h===pe)&&(n=!0,c=t.startsWith(re,d+1)),n?c?h===te&&(n=!1,c=!1):h===be&&(n=!1):(o+=m,o===r&&!u.done&&(e.push(""),o=0)),s=u,u=i.next(),d+=h.length}a=e.at(-1),!o&&a!==void 0&&a.length&&e.length>1&&(e[e.length-2]+=e.pop())},Qe=e=>{const t=e.split(" ");let r=t.length;for(;r&&!N(t[r-1]);)r--;return r===t.length?e:t.slice(0,r).join(" ")+t.slice(r).join("")},et=(e,t,r={})=>{if(r.trim!==!1&&e.trim()==="")return"";let i="",n,c;const a=e.split(" ");let o=[""],s=0;for(let h=0;h<a.length;h++){const m=a[h];if(r.trim!==!1){const f=o.at(-1)??"",b=f.trimStart();f.length!==b.length&&(o[o.length-1]=b,s=N(b))}h!==0&&(s>=t&&(r.wordWrap===!1||r.trim===!1)&&(o.push(""),s=0),(s||r.trim===!1)&&(o[o.length-1]+=" ",s++));const p=N(m);if(r.hard&&p>t){const f=t-s,b=1+Math.floor((p-f-1)/t);Math.floor((p-1)/t)<b&&o.push(""),se(o,m,t),s=N(o.at(-1)??"");continue}if(s+p>t&&s&&p){if(r.wordWrap===!1&&s<t){se(o,m,t),s=N(o.at(-1)??"");continue}o.push(""),s=0}if(s+p>t&&r.wordWrap===!1){se(o,m,t),s=N(o.at(-1)??"");continue}o[o.length-1]+=m,s+=p}r.trim!==!1&&(o=o.map(h=>Qe(h)));const u=o.join(`
|
|
3
|
+
`);let d=!1;for(let h=0;h<u.length;h++){const m=u[h];if(i+=m,d)d=!1;else if(d=m>="\uD800"&&m<="\uDBFF",d)continue;if(m===z||m===pe){ye.lastIndex=h+1;const f=ye.exec(u)?.groups;if(f?.code!==void 0){const b=Number.parseFloat(f.code);n=b===Xe?void 0:b}else f?.uri!==void 0&&(c=f.uri.length===0?void 0:f.uri)}if(u[h+1]===`
|
|
4
|
+
`){c&&(i+=we(""));const p=n?$e(n):void 0;n&&p&&(i+=xe(p))}else m===`
|
|
5
|
+
`&&(n&&$e(n)&&(i+=xe(n)),c&&(i+=we(c)))}return i},tt=/\r?\n/;function J(e,t,r){return String(e).normalize().split(tt).map(i=>et(i,t,r)).join(`
|
|
6
|
+
`)}function rt(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var ie,ve;function st(){if(ve)return ie;ve=1;const e="\x1B",t=`${e}[`,r="\x07",i={to(a,o){return o?`${t}${o+1};${a+1}H`:`${t}${a+1}G`},move(a,o){let s="";return a<0?s+=`${t}${-a}D`:a>0&&(s+=`${t}${a}C`),o<0?s+=`${t}${-o}A`:o>0&&(s+=`${t}${o}B`),s},up:(a=1)=>`${t}${a}A`,down:(a=1)=>`${t}${a}B`,forward:(a=1)=>`${t}${a}C`,backward:(a=1)=>`${t}${a}D`,nextLine:(a=1)=>`${t}E`.repeat(a),prevLine:(a=1)=>`${t}F`.repeat(a),left:`${t}G`,hide:`${t}?25l`,show:`${t}?25h`,save:`${e}7`,restore:`${e}8`},n={up:(a=1)=>`${t}S`.repeat(a),down:(a=1)=>`${t}T`.repeat(a)},c={screen:`${t}2J`,up:(a=1)=>`${t}1J`.repeat(a),down:(a=1)=>`${t}J`.repeat(a),line:`${t}2K`,lineEnd:`${t}K`,lineStart:`${t}1K`,lines(a){let o="";for(let s=0;s<a;s++)o+=this.line+(s<a-1?i.up():"");return a&&(o+=i.left),o}};return ie={cursor:i,scroll:n,erase:c,beep:r},ie}var P=st();function Y(e,t,r){if(!r.some(a=>!a.disabled))return e;const i=e+t,n=Math.max(r.length-1,0),c=i<0?n:i>n?0:i;return r[c]?.disabled?Y(c,t<0?-1:1,r):c}const it=["up","down","left","right","space","enter","cancel"],W={actions:new Set(it),aliases:new Map([["k","up"],["j","down"],["h","left"],["l","right"],["","cancel"],["escape","cancel"]]),withGuide:!0};function nt(e){if(e!==void 0)return e;const t=process.env.ACCESSIBLE;return t!==void 0&&t!==""&&t!=="0"&&t!=="false"}function Se(e,t){if(typeof e=="string")return W.aliases.get(e)===t;for(const r of e)if(r!==void 0&&Se(r,t))return!0;return!1}function at(e,t){if(e===t)return;const r=e.split(`
|
|
7
7
|
`),i=t.split(`
|
|
8
|
-
`),n=Math.max(r.length,i.length),c=[];for(let a=0;a<n;a++)r[a]!==i[a]&&c.push(a);return{lines:c,numLinesBefore:r.length,numLinesAfter:i.length,numLines:n}}globalThis.process.platform.startsWith("win");const
|
|
9
|
-
`).map((o,s,
|
|
10
|
-
`)}function
|
|
8
|
+
`),n=Math.max(r.length,i.length),c=[];for(let a=0;a<n;a++)r[a]!==i[a]&&c.push(a);return{lines:c,numLinesBefore:r.length,numLinesAfter:i.length,numLines:n}}globalThis.process.platform.startsWith("win");const ne=Symbol("clack:cancel");function K(e){return e===ne}function q(e,t){const r=e;r.isTTY&&r.setRawMode(t)}const Ee=e=>"columns"in e&&typeof e.columns=="number"?e.columns:80,_e=e=>"rows"in e&&typeof e.rows=="number"?e.rows:20;function ae(e,t,r,i=r,n=r,c){const a=Ee(e??he);return J(t,a-r.length,{hard:!0,trim:!1}).split(`
|
|
9
|
+
`).map((o,s,u)=>{const d=o;return s===0?`${i}${d}`:s===u.length-1?`${n}${d}`:`${r}${d}`}).join(`
|
|
10
|
+
`)}function ot(e,t){if("~standard"in e){const r=e["~standard"].validate(t);return r instanceof Promise?r.then(i=>i.issues?.at(0)?.message):r.issues?.at(0)?.message}return e(t)}class Ie{input;output;_abortSignal;rl;opts;_render;_track=!1;_prevFrame="";_subscribers=new Map;_cursor=0;state="initial";error="";value;userInput="";get accessible(){return nt(this.opts.accessible)}constructor(t,r=!0){const{input:i=Le,output:n=he,render:c,signal:a,...o}=t;this.opts=o,this.onKeypress=this.onKeypress.bind(this),this.close=this.close.bind(this),this.render=this.render.bind(this),this._render=c.bind(this),this._track=r,this._abortSignal=a,this.input=i,this.output=n}unsubscribe(){this._subscribers.clear()}setSubscriber(t,r){const i=this._subscribers.get(t)??[];i.push(r),this._subscribers.set(t,i)}on(t,r){this.setSubscriber(t,{cb:r})}once(t,r){this.setSubscriber(t,{cb:r,once:!0})}emit(t,...r){const i=this._subscribers.get(t)??[],n=[];for(const c of i)c.cb(...r),c.once&&n.push(()=>i.splice(i.indexOf(c),1));for(const c of n)c()}prompt(){return new Promise(t=>{if(this._abortSignal){if(this._abortSignal.aborted)return this.state="cancel",this.close(),t(ne);this._abortSignal.addEventListener("abort",()=>{this.state="cancel",this.close()},{once:!0})}this.rl=Ne.createInterface({input:this.input,tabSize:2,prompt:"",escapeCodeTimeout:50,terminal:!0}),this.rl.prompt(),this.opts.initialUserInput!==void 0&&this._setUserInput(this.opts.initialUserInput,!0),this.input.on("keypress",this.onKeypress),q(this.input,!0),this.output.on("resize",this.render),this.render(),this.once("submit",()=>{this.output.write(P.cursor.show),this.output.off("resize",this.render),q(this.input,!1),t(this.value)}),this.once("cancel",()=>{this.output.write(P.cursor.show),this.output.off("resize",this.render),q(this.input,!1),t(ne)})})}_isActionKey(t,r){return t===" "}_shouldSubmit(t,r){return!0}_setValue(t){this.value=t,this.emit("value",this.value)}_setUserInput(t,r){this.userInput=t??"",this.emit("userInput",this.userInput),r&&this._track&&this.rl&&(this.rl.write(this.userInput),this._cursor=this.rl.cursor)}_clearUserInput(){this.rl?.write(null,{ctrl:!0,name:"u"}),this._setUserInput("")}async onKeypress(t,r){if(this.state!=="validating"){if(this._track&&r.name!=="return"&&(r.name&&this._isActionKey(t,r)&&this.rl?.write(null,{ctrl:!0,name:"h"}),this._cursor=this.rl?.cursor??0,this._setUserInput(this.rl?.line)),this.state==="error"&&(this.state="active"),r?.name&&(!this._track&&W.aliases.has(r.name)&&this.emit("cursor",W.aliases.get(r.name)),W.actions.has(r.name)&&this.emit("cursor",r.name)),t&&(t.toLowerCase()==="y"||t.toLowerCase()==="n")&&this.emit("confirm",t.toLowerCase()==="y"),this.emit("key",t,r),r?.name==="return"&&this._shouldSubmit(t,r)){if(this.opts.validate){const i=ot(this.opts.validate,this.value);let n;i instanceof Promise?(this.state="validating",this.render(),n=await i):n=i,n&&(this.error=n instanceof Error?n.message:n,this.state="error",this.rl?.write(this.userInput))}this.state!=="error"&&(this.state="submit")}Se([t,r?.name,r?.sequence],"cancel")&&(this.state="cancel"),(this.state==="submit"||this.state==="cancel")&&this.emit("finalize"),this.render(),(this.state==="submit"||this.state==="cancel")&&this.close()}}close(){this.input.unpipe(),this.input.removeListener("keypress",this.onKeypress),this.output.write(`
|
|
11
11
|
`),q(this.input,!1),this.rl?.close(),this.rl=void 0,this.emit(`${this.state}`,this.value),this.unsubscribe()}restoreCursor(){const t=J(this._prevFrame,process.stdout.columns,{hard:!0,trim:!1}).split(`
|
|
12
|
-
`).length-1;this.output.write(P.cursor.move(-999,t*-1))}render(){const t=J(this._render(this)??"",process.stdout.columns,{hard:!0,trim:!1});if(t!==this._prevFrame){if(this.state==="initial")this.output.write(P.cursor.hide);else{const r=
|
|
12
|
+
`).length-1;this.output.write(P.cursor.move(-999,t*-1))}render(){const t=J(this._render(this)??"",process.stdout.columns,{hard:!0,trim:!1});if(t!==this._prevFrame){if(this.state==="initial")this.output.write(P.cursor.hide);else{const r=at(this._prevFrame,t),i=_e(this.output);if(this.restoreCursor(),r){const n=Math.max(0,r.numLinesAfter-i),c=Math.max(0,r.numLinesBefore-i);let a=r.lines.find(o=>o>=n);if(a===void 0){this._prevFrame=t;return}if(r.lines.length===1){this.output.write(P.cursor.move(0,a-c)),this.output.write(P.erase.lines(1));const o=t.split(`
|
|
13
13
|
`);this.output.write(o[a]),this._prevFrame=t,this.output.write(P.cursor.move(0,o.length-a-1));return}else if(r.lines.length>1){if(n<c)a=n;else{const s=a-c;s>0&&this.output.write(P.cursor.move(0,s))}this.output.write(P.erase.down());const o=t.split(`
|
|
14
14
|
`).slice(a);this.output.write(o.join(`
|
|
15
|
-
`)),this._prevFrame=t;return}}this.output.write(P.erase.down())}this.output.write(t),this.state==="initial"&&(this.state="active"),this._prevFrame=t}}}let
|
|
16
|
-
`);F.push(E),
|
|
17
|
-
`);if(h.length>0){const[m,...p]=h;m.length>0?a.push(`${
|
|
15
|
+
`)),this._prevFrame=t;return}}this.output.write(P.erase.down())}this.output.write(t),this.state==="initial"&&(this.state="active"),this._prevFrame=t}}}let ct=class extends Ie{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?Y(i,1,this.options):i,this.changeValue(),this.on("cursor",n=>{switch(n){case"left":case"up":this.cursor=Y(this.cursor,-1,this.options);break;case"down":case"right":this.cursor=Y(this.cursor,1,this.options);break}this.changeValue()})}};class lt extends Ie{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}${l("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 ut(){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 ht=ut(),j=(e,t)=>ht?e:t,Fe=j("\u25C6","*"),mt=j("\u25A0","x"),dt=j("\u25B2","x"),Ce=j("\u25C7","o"),v=j("\u2502","|"),V=j("\u2514","\u2014"),ft=j("\u25CF",">"),Te=j("\u25CB"," "),pt=j("\u25CF","\u2022"),gt=j("\u25C6","*"),bt=j("\u25B2","!"),yt=j("\u25A0","x"),Re=e=>{switch(e){case"initial":case"active":return l("cyan",Fe);case"cancel":return l("red",mt);case"error":return l("yellow",dt);case"submit":return l("green",Ce);case"validating":return l("dim",Fe)}},$t=e=>{switch(e){case"initial":case"active":return l("cyan",v);case"cancel":return l("red",v);case"error":return l("yellow",v);case"submit":return l("green",v)}};function xt(e,t){const r=[`${t?`${l("cyan",v)} `:""}${e.join(" \u2022 ")}`];return t&&r.push(l("cyan",V)),r}const Ae=(e,t,r,i,n,c=!1)=>{let a=t,o=0;if(c)for(let s=i-1;s>=r;s--){const u=e[s];if(u&&(a-=u.length),o++,a<=n)break}else for(let s=r;s<i;s++){const u=e[s];if(u&&(a-=u.length),o++,a<=n)break}return{lineCount:a,removals:o}},wt=({cursor:e,options:t,style:r,output:i=process.stdout,maxItems:n=Number.POSITIVE_INFINITY,columnPadding:c=0,rowPadding:a=4})=>{const o=Ee(i)-c,s=_e(i),u=l("dim","..."),d=Math.max(s-a,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 p=h<t.length&&m>0,f=h<t.length&&m+h<t.length;const b=Math.min(m+h,t.length),F=[];let T=0;p&&T++,f&&T++;const O=m+(p?1:0),M=b-(f?1:0);for(let w=O;w<M;w++){const S=t[w],x=S?r(S,w===e):"",E=J(x,o,{hard:!0,trim:!1}).split(`
|
|
16
|
+
`);F.push(E),T+=E.length}if(T>d){let w=0,S=0,x=T;const E=e-O;let _=d;const R=()=>Ae(F,x,0,E,_),I=()=>Ae(F,x,E+1,F.length,_,!0);p?({lineCount:x,removals:w}=R(),x>_&&(f||(_-=1),{lineCount:x,removals:S}=I())):(f||(_-=1),{lineCount:x,removals:S}=I(),x>_&&(_-=1,{lineCount:x,removals:w}=R())),w>0&&(p=!0,F.splice(0,w)),S>0&&(f=!0,F.splice(F.length-S,S))}const k=[];p&&k.push(u);for(const w of F)for(const S of w)k.push(S);return f&&k.push(u),k};`${l("dim","\u2191/\u2193")}`,`${l("dim","Space:")}`,`${l("dim","Enter:")}`;const D={message:(e=[],{symbol:t=l("gray",v),secondarySymbol:r=l("gray",v),output:i=process.stdout,spacing:n=1,withGuide:c}={})=>{const a=[],o=c??W.withGuide,s=o?r:"",u=o?`${t} `:"",d=o?`${r} `:"";for(let m=0;m<n;m++)a.push(s);const h=Array.isArray(e)?e:e.split(`
|
|
17
|
+
`);if(h.length>0){const[m,...p]=h;m.length>0?a.push(`${u}${m}`):a.push(o?t:"");for(const f of p)f.length>0?a.push(`${d}${f}`):a.push(o?r:"")}i.write(`${a.join(`
|
|
18
18
|
`)}
|
|
19
|
-
`)},info:(e,t)=>{D.message(e,{...t,symbol:
|
|
19
|
+
`)},info:(e,t)=>{D.message(e,{...t,symbol:l("blue",pt)})},success:(e,t)=>{D.message(e,{...t,symbol:l("green",gt)})},step:(e,t)=>{D.message(e,{...t,symbol:l("green",Ce)})},warn:(e,t)=>{D.message(e,{...t,symbol:l("yellow",bt)})},warning:(e,t)=>{D.warn(e,t)},error:(e,t)=>{D.message(e,{...t,symbol:l("red",yt)})}},vt=(e="",t)=>{const r=process.stdout,i=`${l("gray",V)} `;r.write(`${i}${l("red",e)}
|
|
20
20
|
|
|
21
|
-
`)},
|
|
22
|
-
${
|
|
21
|
+
`)},St=(e="",t)=>{const r=process.stdout,i=`${l("gray",v)}
|
|
22
|
+
${l("gray",V)} `;r.write(`${i}${e}
|
|
23
23
|
|
|
24
|
-
`)},
|
|
24
|
+
`)},Et=[`${l("dim","\u2191/\u2193")} to navigate`,`${l("dim","Enter:")} confirm`],X=(e,t)=>e.includes(`
|
|
25
25
|
`)?e.split(`
|
|
26
26
|
`).map(r=>t(r)).join(`
|
|
27
|
-
`):t(e),
|
|
27
|
+
`):t(e),oe=e=>{const t=(i,n)=>{if(i===void 0)return"";const c=i.label??String(i.value);switch(n){case"disabled":return`${l("gray",Te)} ${X(c,a=>l("gray",a))}${i.hint?` ${l("dim",`(${i.hint??"disabled"})`)}`:""}`;case"selected":return`${X(c,a=>l("dim",a))}`;case"active":return`${l("green",ft)} ${c}${i.hint?` ${l("dim",`(${i.hint})`)}`:""}`;case"cancelled":return`${X(c,a=>l(["strikethrough","dim"],a))}`;default:return`${l("dim",Te)} ${X(c,a=>l("dim",a))}`}},r=e.showInstructions??!0;return new ct({options:e.options,signal:e.signal,input:e.input,output:e.output,initialValue:e.initialValue,render(){const i=e.withGuide??W.withGuide,n=`${Re(this.state)} `,c=`${$t(this.state)} `,a=ae(e.output,e.message,c,n),o=`${i?`${l("gray",v)}
|
|
28
28
|
`:""}${a}
|
|
29
|
-
`;switch(this.state){case"submit":{const s=i?`${
|
|
30
|
-
${
|
|
31
|
-
`).length,d=r?xt(
|
|
32
|
-
`),m=d.length+1;return`${o}${s}${
|
|
29
|
+
`;switch(this.state){case"submit":{const s=i?`${l("gray",v)} `:"",u=ae(e.output,t(this.options[this.cursor],"selected"),s);return`${o}${u}`}case"cancel":{const s=i?`${l("gray",v)} `:"",u=ae(e.output,t(this.options[this.cursor],"cancelled"),s);return`${o}${u}${i?`
|
|
30
|
+
${l("gray",v)}`:""}`}default:{const s=i?`${l("cyan",v)} `:"",u=o.split(`
|
|
31
|
+
`).length,d=r?xt(Et,i):i?[l("cyan",V)]:[],h=d.join(`
|
|
32
|
+
`),m=d.length+1;return`${o}${s}${wt({output:e.output,cursor:this.cursor,options:this.options,maxItems:e.maxItems,columnPadding:s.length,rowPadding:u+m,style:(p,f)=>t(p,p.disabled?"disabled":f?"active":"inactive")}).join(`
|
|
33
33
|
${s}`)}
|
|
34
34
|
${h}
|
|
35
|
-
`}}}}).prompt()};`${
|
|
36
|
-
`:""}${
|
|
37
|
-
`,i=e.placeholder&&e.placeholder.length>0?
|
|
35
|
+
`}}}}).prompt()};`${l("gray",v)}`;const ke=e=>new lt({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??W.withGuide,r=`${`${t?`${l("gray",v)}
|
|
36
|
+
`:""}${Re(this.state)} `}${e.message}
|
|
37
|
+
`,i=e.placeholder&&e.placeholder.length>0?l("inverse",e.placeholder[0])+l("dim",e.placeholder.slice(1)):l(["inverse","hidden"],"_"),n=this.userInput?this.userInputWithCursor:i,c=this.value??"";switch(this.state){case"validating":{const a=t?`${l("cyan",v)} `:"",o=t?l("cyan",V):"",s=l("dim",n),u=l("dim","Validating...");return`${r}${a}${s}
|
|
38
|
+
${o} ${u}
|
|
39
|
+
`}case"error":{const a=this.error?` ${l("yellow",this.error)}`:"",o=t?`${l("yellow",v)} `:"",s=t?l("yellow",V):"";return`${r.trim()}
|
|
38
40
|
${o}${n}
|
|
39
41
|
${s}${a}
|
|
40
|
-
`}case"submit":{const a=c
|
|
41
|
-
${o}`:""}`}default:{const a=t?`${
|
|
42
|
+
`}case"submit":{const a=c?`${t?" ":""}${l("dim",c)}`:"",o=t?l("gray",v):"";return`${r}${o}${a}`}case"cancel":{const a=c?` ${l(["strikethrough","dim"],c)}`:"",o=t?l("gray",v):"";return`${r}${o}${a}${c.trim()?`
|
|
43
|
+
${o}`:""}`}default:{const a=t?`${l("cyan",v)} `:"",o=t?l("cyan",V):"";return`${r}${a}${n}
|
|
42
44
|
${o}
|
|
43
|
-
`}}}}).prompt();function
|
|
45
|
+
`}}}}).prompt();function ce(e){return e==null?[]:Array.isArray(e)?e:[e]}function _t(e,t,r,i){var n,c=e[t],a=~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]=c==null?a:Array.isArray(c)?c.concat(a):[c,a]}function It(e,t){e=e||[],t=t||{};var r,i,n,c,a,o={_:[]},s=0,u=0,d=0,h=e.length;const m=t.alias!==void 0,p=t.unknown!==void 0,f=t.default!==void 0;if(t.alias=t.alias||{},t.string=ce(t.string),t.boolean=ce(t.boolean),m)for(r in t.alias)for(i=t.alias[r]=ce(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]]||[],u=i.length;u-- >0;)t.boolean.push(i[u]);for(s=t.string.length;s-- >0;)for(i=t.alias[t.string[s]]||[],u=i.length;u-- >0;)t.string.push(i[u]);if(f){for(r in t.default)if(c=typeof t.default[r],i=t.alias[r]=t.alias[r]||[],t[c]!==void 0)for(t[c].push(r),s=0;s<i.length;s++)t[c].push(i[s])}const b=p?Object.keys(t.alias):[];for(s=0;s<h;s++){if(n=e[s],n==="--"){o._=o._.concat(e.slice(++s));break}for(u=0;u<n.length&&n.charCodeAt(u)===45;u++);if(u===0)o._.push(n);else if(n.substring(u,u+3)==="no-"){if(c=n.substring(u+3),p&&!~b.indexOf(c))return t.unknown(n);o[c]=!1}else{for(d=u+1;d<n.length&&n.charCodeAt(d)!==61;d++);for(c=n.substring(u,d),a=n.substring(++d)||s+1===h||(""+e[s+1]).charCodeAt(0)===45||e[++s],i=u===2?[c]:c,d=0;d<i.length;d++){if(c=i[d],p&&!~b.indexOf(c))return t.unknown("-".repeat(u)+c);_t(o,c,d+1<i.length||a,t)}}}if(f)for(r in t.default)o[r]===void 0&&(o[r]=t.default[r]);if(m)for(r in o)for(i=t.alias[r]||[];i.length>0;)o[i.shift()]=o[r];return o}var Z={exports:{}},je;function Ft(){if(je)return Z.exports;je=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=(o,s,u=o)=>d=>{let h=""+d,m=h.indexOf(s,o.length);return~m?o+c(h,s,u,m)+s:o+h+s},c=(o,s,u,d)=>{let h="",m=0;do h+=o.substring(m,d)+u,m=d+s.length,d=o.indexOf(s,m);while(~d);return h+o.substring(m)},a=(o=i)=>{let s=o?n:()=>String;return{isColorSupported:o,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 Z.exports=a(),Z.exports.createColors=a,Z.exports}var Ct=Ft();const Tt=rt(Ct),{cyan:Rt,magenta:At,yellow:kt}=Tt,H=[{name:"engine",display:"Engine",description:"with the PlayCanvas Engine API directly",color:kt},{name:"react",display:"React",description:"with @playcanvas/react",color:Rt},{name:"web-components",display:"Web Components",description:"with @playcanvas/web-components",color:At}],Oe={physics:{engine:["assets"],react:["react"],"web-components":["assets"]}},U=[{name:"spinning-cube",display:"Spinning Cube",category:"Basics",description:"a rotating cube and a light"},{name:"interactive-sphere",display:"Interactive Sphere",category:"Basics",description:"orbit controls, a grid and pointer picking"},{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:"physics-playground",display:"Physics Playground",category:"Games",description:"rigid bodies, collisions, spawning and reset",features:["physics"]},{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:"scene-editor",display:"Scene Editor",category:"Tools",description:"selection and translate, rotate and scale gizmos"},{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"}],jt=async({argFormat:e,yes:t,cancel:r})=>{if(e&&H.some(n=>n.name===e))return e;if(e&&(!process.stdout.isTTY||t))throw new Error(`Unknown format: ${e}. Choose from: ${H.map(n=>n.name).join(", ")}`);if(t)return H[0].name;const i=await oe({message:e?`"${e}" isn't a valid format. Please choose from below: `:"Select a format:",options:H.map(n=>({label:n.color(n.display),value:n.name,hint:n.description}))});return K(i)&&r(),i},Ot=async({argStarter:e,yes:t,cancel:r})=>{if(e&&U.some(n=>n.name===e))return e;if(!process.stdout.isTTY||t){if(!e)return U[0].name;throw new Error(`Unknown starter: ${e}. Choose from: ${U.map(n=>n.name).join(", ")}`)}const i=await oe({message:e?`"${e}" isn't a valid starter. Please choose from below: `:"Select a starter:",initialValue:U[0].name,options:U.map(n=>({label:n.display,value:n.name,hint:`${n.category} \xB7 ${n.description}`}))});return K(i)&&r(),i},Be=e=>/^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(e),le=e=>e.trim().toLowerCase().replace(/\s+/g,"-").replace(/^[._]/,"").replace(/[^a-z\d\-~]+/g,"-"),Bt=async({targetDir:e,yes:t,cancel:r})=>{let i=y.basename(y.resolve(e));if(!Be(i)){if(t)return le(i);const n=await ke({message:"Package name:",defaultValue:le(i),placeholder:le(i),validate:c=>{if(!c||!Be(c))return"Invalid package.json name"}});if(K(n))return r();i=n}return i},Q=e=>e.trim().replace(/\/+$/g,""),Mt=e=>{const t=$.readdirSync(e);return t.length===0||t.length===1&&t[0]===".git"},Pt=e=>{if($.existsSync(e))for(const t of $.readdirSync(e))t!==".git"&&$.rmSync(y.resolve(e,t),{recursive:!0,force:!0})},Lt=async({argTargetDir:e,defaultTargetDir:t,yes:r,cancel:i})=>{let n=e?Q(String(e)):void 0;if(!n&&r)return t;if(!n){const c=await ke({message:"Project name:",defaultValue:t,placeholder:t,validate:a=>{if(a&&!Q(a))return"Invalid project name"}});K(c)&&i(),n=Q(c)}return n},Nt=async({targetDir:e,argOverwrite:t,yes:r,cancel:i})=>{if($.existsSync(e)&&!Mt(e)){if(r&&!t)throw new Error(`Target directory "${e}" is not empty. Pass --overwrite to remove its contents.`);const n=t?"yes":await oe({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:"Ignore files and continue",value:"ignore"}]});if(K(n))return i();switch(n){case"yes":Pt(e);break;case"no":i();return}}},Dt=["scripts","dependencies","devDependencies","peerDependencies","engines"],Me="## Prerequisites",Wt={engine:"src/main.ts",react:"src/App.tsx","web-components":"src/main.ts"},Vt=e=>{const{cwd:t,targetDir:r,format:i,starter:n,packageName:c,skills:a,renameFiles:o}=e,s=y.resolve(t,r);$.mkdirSync(s,{recursive:!0});const u=process.env.npm_config_user_agent?.split("/")[0]??"npm";D.step(`Creating project in ${s}...`);const d=De(import.meta.url),h=y.dirname(d),m=[y.resolve(h,"../templates"),y.resolve(h,"../../templates")].find(g=>$.existsSync(y.join(g,i)));if(!m)throw new Error(`Format directory not found for: ${i}`);const p=y.join(m,i);for(const g of H)for(const C of U){const A=y.join(m,g.name,C.name),G=Wt[g.name];if(!G||!$.existsSync(y.join(A,G)))throw new Error(`Incomplete starter matrix: ${g.name}/${C.name}`);for(const ee of C.features??[]){const ue=Oe[ee]?.[g.name];if(!ue?.length||ue.some(Pe=>!$.existsSync(y.join(m,"_features",ee,Pe))))throw new Error(`Incomplete feature matrix: ${ee}/${g.name}`)}}const f=U.find(g=>g.name===n);if(!f)throw new Error(`Starter not found: ${n}`);const b=y.join(m,"_shared",n),F=y.join(p,"base"),T=y.join(p,n),O=(f.features??[]).flatMap(g=>Oe[g][i].map(C=>y.join(m,"_features",g,C))),M=[b,F,...O,T],k=M.map(g=>{const C=y.join(g,"package.json");return $.existsSync(C)?JSON.parse($.readFileSync(C,"utf-8")):{}}),w=[k[1],k[0],...k.slice(2)];if(!$.existsSync(T))throw new Error(`Starter directory not found: ${i}/${n}`);for(const g of M)$.existsSync(g)&&$.cpSync(g,s,{recursive:!0});const S=y.join(b,"README.md"),x=y.join(s,"README.md");$.existsSync(S)&&$.writeFileSync(x,$.readFileSync(x,"utf8").replace(Me,`${$.readFileSync(S,"utf8").trimEnd()}
|
|
44
46
|
|
|
45
|
-
${Me}`));for(const[g,
|
|
46
|
-
`),a){const g=[y.resolve(h,"../skills"),y.resolve(h,"../../skills")].find(A
|
|
47
|
+
${Me}`));for(const[g,C]of Object.entries(o)){const A=y.join(s,g);$.existsSync(A)&&$.renameSync(A,y.join(s,C))}const E=y.join(s,"package.json"),_=Object.assign({},...w);for(const g of Dt){const C=Object.assign({},...w.map(A=>A?.[g]));Object.keys(C).length?_[g]=C:delete _[g]}if(_.name=c,$.writeFileSync(E,JSON.stringify(_,null,4)+`
|
|
48
|
+
`),a){const g=[y.resolve(h,"../skills"),y.resolve(h,"../../skills")].find(A=>$.existsSync(A));if(!g)throw new Error("Skills directory not found");const C=$.readdirSync(g,{withFileTypes:!0}).filter(A=>A.isDirectory()).map(A=>A.name);for(const A of[".claude/skills",".agents/skills"])for(const G of C)$.cpSync(y.join(g,G),y.join(s,A,G),{recursive:!0});D.step("Added PlayCanvas agent skills (opt out with --no-skills)")}let R=`Project created. Now run:
|
|
47
49
|
`;const I=y.isAbsolute(r)?s:y.relative(t,s);switch(s!==t&&(R+=`
|
|
48
|
-
cd ${I.includes(" ")?`"${I}"`:I}`),
|
|
50
|
+
cd ${I.includes(" ")?`"${I}"`:I}`),u){case"yarn":R+=`
|
|
49
51
|
yarn`,R+=`
|
|
50
52
|
yarn dev`;break;default:R+=`
|
|
51
|
-
${
|
|
52
|
-
${
|
|
53
|
+
${u} install`,R+=`
|
|
54
|
+
${u} run dev`;break}St(R)},L=It(process.argv.slice(2),{alias:{b:"boilerplate",f:"format",h:"help",s:"starter",t:"template",y:"yes"},boolean:["help","overwrite","yes"],string:["format","starter","template","boilerplate"]}),Ut=process.cwd(),Gt=`Usage: npm create playcanvas@latest [OPTION]...
|
|
53
55
|
|
|
54
56
|
Create a new PlayCanvas project with TypeScript.
|
|
55
57
|
With no arguments, start the CLI in interactive mode.
|
|
@@ -61,4 +63,4 @@ Options:
|
|
|
61
63
|
--no-skills omit the PlayCanvas agent skills
|
|
62
64
|
-y, --yes skip the prompts and take the defaults
|
|
63
65
|
-h, --help show help
|
|
64
|
-
`,
|
|
66
|
+
`,Kt={_gitignore:".gitignore",_npmrc:".npmrc"},Ht="playcanvas-project",zt=async()=>{const e=L._[0]?Q(String(L._[0])):void 0,t=L.format??L.template,r=L.starter??L.boilerplate,i=L.overwrite,n=L.yes;if(L.help){console.log(Gt);return}const c=()=>{vt("Operation cancelled"),process.exit(0)},a=await Lt({argTargetDir:e,defaultTargetDir:Ht,yes:n,cancel:c});await Nt({targetDir:a,argOverwrite:i,yes:n,cancel:c});const o=await Bt({targetDir:a,yes:n,cancel:c}),s=await jt({argFormat:t,yes:n,cancel:c}),u=await Ot({argStarter:r,yes:n,cancel:c}),d=L.skills??!0;Vt({cwd:Ut,targetDir:a,format:s,starter:u,packageName:o,skills:d,renameFiles:Kt})};zt().catch(e=>{console.error(e instanceof Error?e.message:e),process.exitCode=1});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-playcanvas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Scaffold a PlayCanvas project",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"playcanvas",
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"typecheck": "tsc --noEmit"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@clack/prompts": "^1.
|
|
45
|
+
"@clack/prompts": "^1.8.1",
|
|
46
46
|
"@playcanvas/eslint-config": "3.0.0-beta.8",
|
|
47
|
-
"@types/node": "22.20.
|
|
47
|
+
"@types/node": "22.20.4",
|
|
48
48
|
"eslint": "^9.39.5",
|
|
49
49
|
"mri": "^1.2.0",
|
|
50
50
|
"picocolors": "^1.1.1",
|
|
51
|
-
"prettier": "^3.9.
|
|
51
|
+
"prettier": "^3.9.9",
|
|
52
52
|
"unbuild": "^3.6.1"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
@@ -5,8 +5,7 @@ description: Use when adding transient visual effects or trails to a PlayCanvas
|
|
|
5
5
|
|
|
6
6
|
# Effects and game feel
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
permanent scene fixtures.
|
|
8
|
+
Trigger transient effects through application events and reuse their resources.
|
|
10
9
|
|
|
11
10
|
Reuse before authoring. Adapt the closest official particle example with `find-examples`, prefer the
|
|
12
11
|
Engine's built-in particle component over a hand-written system, and discover any shipped trail or
|
|
@@ -25,13 +24,23 @@ the actual scene lighting and surface.
|
|
|
25
24
|
|
|
26
25
|
## Place and layer
|
|
27
26
|
|
|
28
|
-
-
|
|
29
|
-
transform once, and let it destroy itself when finished. Never leak emitters.
|
|
27
|
+
- Place an effect at the emitter's world mount point; parent it there or copy the world transform once.
|
|
30
28
|
- Keep additive or transparent effects above the surface they sit on and biased toward the camera so
|
|
31
29
|
they do not z-fight. Inspect soft transparency at grazing angles for hard edges, black quads, or
|
|
32
30
|
dropout.
|
|
33
31
|
- Size and orient effects from the emitter's calibrated bounds, not a guessed constant.
|
|
34
32
|
|
|
33
|
+
## Pool and prewarm
|
|
34
|
+
|
|
35
|
+
- Create emitters, ribbons, flashes, materials, and meshes at load. Events check out an effect, set
|
|
36
|
+
its transform, and call `particlesystem.reset()` then `play()`; after its particles finish, call
|
|
37
|
+
`stop()` and return it to the pool.
|
|
38
|
+
- First-use shader compilation can stall a frame. Before gameplay, render every material and particle
|
|
39
|
+
variant through the relevant passes, on an offscreen rig or behind the ready overlay.
|
|
40
|
+
- Pulse or tint uniforms with `meshInstance.setParameter` or `material.setParameter`.
|
|
41
|
+
`material.update()` marks the whole material dirty and clears variants when defines or chunks
|
|
42
|
+
change; reserve it for material changes that require it.
|
|
43
|
+
|
|
35
44
|
## Moving trails
|
|
36
45
|
|
|
37
46
|
- Treat built-in render primitives as blocking geometry for soft effects. Follow the closest
|
|
@@ -74,8 +83,10 @@ the actual scene lighting and surface.
|
|
|
74
83
|
|
|
75
84
|
Trigger each effect through real gameplay input and confirm with a screenshot captured at its peak,
|
|
76
85
|
not a saved filepath. Confirm the effect is visible from the gameplay camera and from a grazing
|
|
77
|
-
angle, that
|
|
78
|
-
|
|
86
|
+
angle, that repeated events leave entity counts stable, and that nothing leaves a black quad or a
|
|
87
|
+
hard edge over the surface. Compare `graphicsDevice.shaders.length` after prewarming and after the
|
|
88
|
+
first real event; growth indicates a missed variant. Use two fixed poses and return screenshots at
|
|
89
|
+
accept-or-reject points only, not after every edit.
|
|
79
90
|
|
|
80
91
|
Choose the authoring surface with the `build-app` skill; create effects through its entity and
|
|
81
92
|
component primitives and own their lifecycle in the surface that spawned them.
|
|
@@ -41,8 +41,8 @@ details. Apply these stable conventions throughout the project.
|
|
|
41
41
|
with real input after a rendered frame. Judge the camera basis or a projected fixed landmark, not
|
|
42
42
|
world-space camera coordinates or internal angle signs; both can approve inverted behavior.
|
|
43
43
|
- Materials cull back faces by default; counter-clockwise winding defines the front face.
|
|
44
|
-
- Clone a material before
|
|
45
|
-
|
|
44
|
+
- Clone and reassign a shared material before editing it for only some meshes, including default
|
|
45
|
+
and asset-imported materials. Per-mesh uniforms use `meshInstance.setParameter()` without cloning.
|
|
46
46
|
- `setParameter` uploads raw values; author colours in sRGB and convert with `Color#linear()`
|
|
47
47
|
before upload — do not hand-roll gamma.
|
|
48
48
|
- Entities do not have an `aabb`. Union descendant `meshInstance.aabb` values after transforms have
|
|
@@ -26,26 +26,32 @@ shadow-map rendering and per-pixel dynamic lighting for that light entirely.
|
|
|
26
26
|
|
|
27
27
|
## Verify UVs before trusting this on real assets
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
The installed `Lightmapper` bakes a node only when every mesh instance on it has a second UV set
|
|
30
|
+
(`mesh.vertexBuffer.format.hasUv1`). A node missing one is skipped silently — no warning, no
|
|
31
|
+
lightmap — and its mesh simply stays dynamically lit, which a glance at a screenshot will not catch.
|
|
32
|
+
Procedural primitives generate that set; imported GLB models often do not. Check `hasUv1` on every
|
|
33
|
+
mesh you expect to bake, and unwrap or re-export the ones without it before the bake. Do not carry a
|
|
34
|
+
primitive-only test result into production assets unchecked.
|
|
34
35
|
|
|
35
36
|
## Choose the rung by budget, not by default
|
|
36
37
|
|
|
37
|
-
On-device bake cost scales with baked-node count and lightmap resolution;
|
|
38
|
-
|
|
38
|
+
On-device bake cost scales with baked-node count and lightmap resolution; measure it once on the
|
|
39
|
+
target device (`performance.now()` around `bake()`) and compare it with the startup budget before
|
|
40
|
+
choosing a rung. Stay on-device unless:
|
|
39
41
|
|
|
40
|
-
- the
|
|
42
|
+
- the measured bake time does not fit the startup or first-paint budget on the target device, or
|
|
41
43
|
- the result needs stylized material detail beyond lighting that the runtime `Lightmapper` cannot
|
|
42
44
|
produce.
|
|
43
45
|
|
|
44
|
-
Only then move to an offline bake shipped as assets,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
Only then move to an offline bake shipped as assets, and size the pipeline to the scene count. For
|
|
47
|
+
one deterministic scene the whole pipeline is a build script that computes or renders the maps and
|
|
48
|
+
writes them next to the other static assets, plus a loader; that is sufficient. Registry-keyed
|
|
49
|
+
per-asset configs, worker-thread parallelism and a coverage audit that fails the bake on a missing
|
|
50
|
+
output belong to multi-asset pipelines — add them when the second scene arrives, not before.
|
|
51
|
+
|
|
52
|
+
Offline maps are payload. State the compressed size delta in the change description and compare it
|
|
53
|
+
with the download or package budget; a modest lightmap set can double the archive of a small web
|
|
54
|
+
app.
|
|
49
55
|
|
|
50
56
|
## Keep dynamic objects consistent
|
|
51
57
|
|
|
@@ -19,11 +19,13 @@ Preserve the surface that owns the current entry point and lifecycle.
|
|
|
19
19
|
|
|
20
20
|
## Render at an intentional density
|
|
21
21
|
|
|
22
|
-
PlayCanvas defaults `graphicsDevice.maxPixelRatio` to 1 for predictable fill-rate cost.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
PlayCanvas defaults `graphicsDevice.maxPixelRatio` to 1 for predictable fill-rate cost. Set it
|
|
23
|
+
deliberately after the application exists and before its initial automatic resize, and call
|
|
24
|
+
`app.resizeCanvas()` after changing it. Fragment work scales with the square of the ratio, so
|
|
25
|
+
choose the value from a measured frame time on the target display, not from the display alone:
|
|
26
|
+
`Math.min(window.devicePixelRatio, 2)` suits a quality-first viewer on hardware that holds its
|
|
27
|
+
frame budget there, and 1 is the right value wherever it does not. Never cap above
|
|
28
|
+
`window.devicePixelRatio`; a larger value renders identically.
|
|
27
29
|
|
|
28
30
|
Verify the real backbuffer in a browser: for automatic resolution, `canvas.width / canvas.clientWidth`
|
|
29
31
|
and the height ratio should match the chosen pixel ratio. CSS dimensions and a screenshot filepath
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: light-scene
|
|
3
|
-
description: Use when a PlayCanvas scene must read as a deliberate
|
|
3
|
+
description: Use when a PlayCanvas scene must read as a deliberate image rather than a flat asset preview — photoreal and cinematic, or stylized, low-poly, and palette-driven — to set key and ambient lighting, sky and image-based lighting, shadows, tone mapping and exposure, water, and a restrained post-process grade of bloom, fog, and colour.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Cinematic rendering
|
|
@@ -9,6 +9,26 @@ A polished frame is lit and graded, not just populated. Treat lighting, sky, sha
|
|
|
9
9
|
and post as one pass over the assembled scene, and tune every value against a real screenshot rather
|
|
10
10
|
than from memory.
|
|
11
11
|
|
|
12
|
+
## Confirm art direction before implementation
|
|
13
|
+
|
|
14
|
+
Follow the user's specified or approved direction. When it is unresolved, present concrete options
|
|
15
|
+
using references, mockups, or inexpensive rendered variants and get approval before substantial
|
|
16
|
+
implementation. Prefer a side-by-side variant sheet when cheap to produce. Work within the approved
|
|
17
|
+
direction and confirm significant departures before implementing them.
|
|
18
|
+
|
|
19
|
+
## Choose the path from the brief
|
|
20
|
+
|
|
21
|
+
Read the reference before wiring any pipeline. A photoreal or cinematic brief takes the runtime HDR
|
|
22
|
+
path below. A brief whose reference is low-poly, flat-shaded, toon, or palette-driven — or whose
|
|
23
|
+
startup, frame, or payload budget rules out the runtime pipeline — takes the stylized path: banded
|
|
24
|
+
or flat shading through `override-shader-chunks`, a solid or gradient sky, static lighting through
|
|
25
|
+
`bake-lighting`, and no `Water` or `CameraFrame`. A remark such as "look at the water script" is a
|
|
26
|
+
pointer to inspect it; confirm any resulting change of art direction before integration. On either
|
|
27
|
+
path, dynamic objects and their attachments must sample or approximate the baked or ambient occlusion
|
|
28
|
+
so they dim to the same levels as the surfaces around them, grounded with contact shadows.
|
|
29
|
+
|
|
30
|
+
## Runtime HDR path
|
|
31
|
+
|
|
12
32
|
Prefer shipped building blocks over hand-written shaders and passes. Discover them with the
|
|
13
33
|
`reuse-scripts` skill and adapt the closest official example with `find-examples`:
|
|
14
34
|
|
|
@@ -16,32 +36,22 @@ Prefer shipped building blocks over hand-written shaders and passes. Discover th
|
|
|
16
36
|
- a procedural sky and image-based light for ambient colour and reflections;
|
|
17
37
|
- a shadow catcher for grounding subjects that have no lit floor.
|
|
18
38
|
|
|
19
|
-
For an outdoor scene
|
|
20
|
-
`find-examples`. Use `graphics/water.example.mjs` as the
|
|
21
|
-
joins the production `Water` script, sky, sun, `CameraFrame`,
|
|
22
|
-
reflection pipeline. Get that complete frame rendering before
|
|
23
|
-
look
|
|
24
|
-
baseline
|
|
39
|
+
For an outdoor scene whose brief asks for reflective, photoreal water, stop before authoring the
|
|
40
|
+
scene and read both `reuse-scripts` and `find-examples`. Use `graphics/water.example.mjs` as the
|
|
41
|
+
initial composition recipe: it already joins the production `Water` script, sky, sun, `CameraFrame`,
|
|
42
|
+
water layer, scene depth, and reflection pipeline. Get that complete frame rendering before
|
|
43
|
+
replacing its assets or tuning its look; a custom normal-mapped material or tessellated plane is not
|
|
44
|
+
that baseline.
|
|
25
45
|
|
|
26
|
-
|
|
46
|
+
For that water scene, keep both production integrations visible in the implementation:
|
|
27
47
|
|
|
28
48
|
- import and create `Water` from `playcanvas/scripts/esm/water.mjs` on the water entity, preserving
|
|
29
49
|
the example's required render component, camera reference, water layer, depth map, and textures;
|
|
30
50
|
- construct `CameraFrame` from `playcanvas` for the gameplay camera, set a deliberate tone map and
|
|
31
51
|
restrained grade, call `update()`, and destroy it with the application.
|
|
32
52
|
|
|
33
|
-
Do not continue to cosmetic tuning while
|
|
34
|
-
shader, texture, or framebuffer error.
|
|
35
|
-
|
|
36
|
-
## Stylized and prebaked scenes
|
|
37
|
-
|
|
38
|
-
When the art direction is deliberately flat or stylized rather than photoreal, or the platform's
|
|
39
|
-
startup, frame, or payload budget rules out the runtime pipeline above, do not treat `Water`,
|
|
40
|
-
`CameraFrame`, and the sky/IBL bake as mandatory. The sanctioned alternative is baked static
|
|
41
|
-
lighting via the `bake-lighting` skill plus custom material shading via the `override-shader-chunks`
|
|
42
|
-
skill. Either way, dynamic objects and their attachments must sample or approximate the baked
|
|
43
|
-
occlusion so they dim to the same levels as the baked surfaces, grounded with contact shadows. Fall
|
|
44
|
-
back to the runtime pipeline above as the default path when neither trigger holds.
|
|
53
|
+
Do not continue to cosmetic tuning while a required integration is missing or diagnostics report
|
|
54
|
+
a shader, texture, or framebuffer error.
|
|
45
55
|
|
|
46
56
|
## Light and expose
|
|
47
57
|
|
|
@@ -84,11 +94,16 @@ back to the runtime pipeline above as the default path when neither trigger hold
|
|
|
84
94
|
|
|
85
95
|
## Prove the look
|
|
86
96
|
|
|
97
|
+
For changes to an existing look, show old and new at the same pose in one image. If the difference
|
|
98
|
+
is not visible, report that before spending more time tuning.
|
|
99
|
+
|
|
87
100
|
Tune against pixels, not numbers. Capture the intended framing with an image-returning browser
|
|
88
101
|
screenshot tool and inspect the returned image, as `apply-conventions` and the local agent guide
|
|
89
|
-
require; a saved filepath is not evidence.
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
require; a saved filepath is not evidence. Use two fixed poses, capture with a headless browser, and
|
|
103
|
+
return images at accept-or-reject points only, not after every edit. Confirm the subject is exposed
|
|
104
|
+
and readable, shadows are present but not crushed, and the grade is restrained, from the real
|
|
105
|
+
gameplay camera at the target resolution. Confirm the post-process camera keeps rendering as it
|
|
106
|
+
orbits and pitches.
|
|
92
107
|
|
|
93
108
|
Choose the authoring surface with the `build-app` skill and set lights, camera, and scene settings
|
|
94
109
|
through its own primitives before reaching into the Engine.
|
|
@@ -13,12 +13,18 @@ override is the default for everything else, since it is cheaper and stays integ
|
|
|
13
13
|
|
|
14
14
|
## Pin the version, read the source
|
|
15
15
|
|
|
16
|
-
Set `material.shaderChunksVersion` to the installed engine's major.minor (for example `'2.
|
|
16
|
+
Set `material.shaderChunksVersion` to the installed engine's major.minor (for example `'2.22'`)
|
|
17
17
|
before shipping any override. Chunk names and their contracts — what a chunk defines, what globals
|
|
18
18
|
it reads or writes — are version-keyed and change between releases. Read the installed `playcanvas`
|
|
19
19
|
source for the exact chunk being touched; do not recall its contract from memory or an older
|
|
20
20
|
version's documentation.
|
|
21
21
|
|
|
22
|
+
## Clone before overriding a shared material
|
|
23
|
+
|
|
24
|
+
Clone a material that other mesh instances share — including default and asset-imported materials —
|
|
25
|
+
before changing its chunks, properties, or material-level parameters for only some meshes, and assign
|
|
26
|
+
the clone. For per-mesh uniforms, use `meshInstance.setParameter()` without cloning the material.
|
|
27
|
+
|
|
22
28
|
## Set chunks through the documented accessor
|
|
23
29
|
|
|
24
30
|
Use `material.getShaderChunks(SHADERLANGUAGE_GLSL).set(name, source)`, and the WGSL equivalent —
|
|
@@ -15,6 +15,12 @@ Read `app.stats.drawCalls.total` (or the `forward`/`depth`/`shadow` breakdown) b
|
|
|
15
15
|
anything, or drop in `MiniStats` for a live overlay. Every rung below is proved against this number,
|
|
16
16
|
not against intuition about what "looks expensive."
|
|
17
17
|
|
|
18
|
+
Draw count alone does not identify the bottleneck. Test sensitivity to pixel cost by halving the
|
|
19
|
+
effective pixel ratio (`Math.min(graphicsDevice.maxPixelRatio, window.devicePixelRatio)`) and calling
|
|
20
|
+
`app.resizeCanvas()`. Verify the backbuffer shrank and compare frame times at the same scene state.
|
|
21
|
+
If frame time improves materially, prioritize resolution, multisampling and per-pixel shader cost.
|
|
22
|
+
Otherwise profile CPU and GPU work before choosing a rung; restore the original ratio after the test.
|
|
23
|
+
|
|
18
24
|
## Rung 1: stop drawing invisible things
|
|
19
25
|
|
|
20
26
|
An element at opacity 0 still submits a draw call — its mesh instance exists and is still in a
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reuse-scripts
|
|
3
|
-
description: Use before implementing non-core PlayCanvas behavior to discover
|
|
3
|
+
description: Use before implementing non-core PlayCanvas behavior to discover the installed Engine's curated scripts and reuse or adapt those that fit the required behavior and approved art direction.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Engine scripts
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
Before writing behavior from scratch, inspect the curated scripts shipped with the installed
|
|
9
|
+
`playcanvas` package under `scripts/esm/**`. Discover the current set:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
12
|
rg 'static scriptName =' node_modules/playcanvas/scripts/esm \
|
|
@@ -19,35 +19,39 @@ is a `Script`; the parsers below `scripts/esm/parsers` are plain classes registe
|
|
|
19
19
|
handler instead.
|
|
20
20
|
|
|
21
21
|
Only import from `scripts/esm/**`. Legacy sibling directories depend on the global Engine namespace.
|
|
22
|
-
After selecting a script, use `find-examples` to locate its matching versioned Engine example.
|
|
22
|
+
After selecting a script, use `find-examples` to locate its matching versioned Engine example. When
|
|
23
|
+
`node_modules/playcanvas` is a linked or source checkout, its `examples/src/examples/**` are already
|
|
24
|
+
on disk; read them there and skip the fetch.
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
preserve its input signs, angular damping, bounds, and lifecycle invariants in the custom portion
|
|
26
|
-
instead of replacing them from memory.
|
|
26
|
+
## Check fit before integration
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Compare candidates with the required behavior, approved art direction, and runtime constraints.
|
|
29
|
+
Reuse a suitable script directly or adapt its configuration and extension points. If none fits,
|
|
30
|
+
state the limitation and implement only the missing behavior, preserving reusable parts and their
|
|
31
|
+
lifecycle, bounds, and input invariants.
|
|
32
|
+
|
|
33
|
+
For visual behavior, resolve an unclear art direction with the user before substantial implementation.
|
|
34
|
+
Present references, mockups, or inexpensive variants; follow existing approval and confirm significant
|
|
35
|
+
departures. A script's default appearance is only a starting point for assessing its fit.
|
|
36
|
+
|
|
37
|
+
## Adapt the reference integration
|
|
29
38
|
|
|
30
39
|
Treat the selected script source and its closest official example as complementary references:
|
|
31
40
|
|
|
32
41
|
1. Read the source for exports, properties, defaults, fallbacks, required components, and lifecycle.
|
|
33
42
|
2. Read the example for assets, entity references, mesh requirements, layer ordering, scene settings,
|
|
34
43
|
and render-pipeline setup.
|
|
35
|
-
3.
|
|
36
|
-
|
|
44
|
+
3. Integrate the selected behavior with its required components, assets, and passes. Adapt the look
|
|
45
|
+
to the approved direction and preserve defaults and dependencies that still apply.
|
|
37
46
|
4. After a rendered frame, fail on console, shader, or missing-asset diagnostics. Exercise the
|
|
38
47
|
behavior with real input where applicable and inspect returned screenshots from representative
|
|
39
48
|
views at the final backbuffer density.
|
|
40
|
-
5.
|
|
41
|
-
|
|
49
|
+
5. Use the example to check integration correctness and the user's approved direction to judge the
|
|
50
|
+
final look. Report any remaining gap.
|
|
42
51
|
|
|
43
52
|
If no matching example exists, state that and derive the integration from installed source instead
|
|
44
53
|
of inventing it from memory.
|
|
45
54
|
|
|
46
|
-
For an outdoor water scene, start from `graphics/water.example.mjs` as one integration recipe. It
|
|
47
|
-
combines `Water`, `ProceduralSky`, `CameraControls`, `CameraFrame`, a dedicated water layer, scene
|
|
48
|
-
depth, normal and caustics textures, and conservative rendering defaults. Establish that complete
|
|
49
|
-
baseline before adapting its camera, assets, time of day, or water tuning.
|
|
50
|
-
|
|
51
55
|
## Preserve grouped defaults
|
|
52
56
|
|
|
53
57
|
Grouped property updates differ by authoring surface. Read the selected reference and preserve
|
|
@@ -1,37 +1,17 @@
|
|
|
1
1
|
# Direct Engine projects
|
|
2
2
|
|
|
3
|
-
Import the
|
|
4
|
-
|
|
5
|
-
```ts
|
|
6
|
-
import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
|
|
7
|
-
|
|
8
|
-
camera.addComponent('script');
|
|
9
|
-
const controls = camera.script?.create(CameraControls);
|
|
10
|
-
```
|
|
3
|
+
Import the selected class from its installed module, add a script component to the owning entity,
|
|
4
|
+
and attach it with `entity.script.create()`. Preserve an existing script component.
|
|
11
5
|
|
|
12
6
|
Some production helpers are core Engine exports rather than scripts. Follow the matching example's
|
|
13
|
-
construction
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
import { CameraFrame } from 'playcanvas';
|
|
17
|
-
|
|
18
|
-
const frame = new CameraFrame(app, camera.camera!);
|
|
19
|
-
frame.rendering.sceneDepthMap = true;
|
|
20
|
-
frame.update();
|
|
21
|
-
```
|
|
7
|
+
construction and teardown contract instead of forcing them through `script.create`.
|
|
22
8
|
|
|
23
9
|
Keep script constructors at module scope. `create` takes two distinct option keys that are not
|
|
24
10
|
interchangeable: `properties` is assigned straight onto the instance, while `attributes` supplies
|
|
25
11
|
declared `@attribute` fields. Passing the wrong one silently does nothing.
|
|
26
12
|
|
|
27
|
-
Prefer mutating an existing grouped property when only a few fields change
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
import { ProceduralSky } from 'playcanvas/scripts/esm/sky/procedural-sky.mjs';
|
|
31
|
-
|
|
32
|
-
const sky = skyEntity.script?.create(ProceduralSky);
|
|
33
|
-
if (sky) sky.sunLight = sun;
|
|
34
|
-
```
|
|
13
|
+
Prefer mutating an existing grouped property when only a few fields change; preserve other defaults
|
|
14
|
+
and follow any update or rebuild contract in the selected source.
|
|
35
15
|
|
|
36
16
|
If the installed script subpaths have no declarations, declare only the patterns the project uses:
|
|
37
17
|
|
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
# React projects
|
|
2
2
|
|
|
3
|
-
Import the
|
|
4
|
-
|
|
5
|
-
```tsx
|
|
6
|
-
import { Entity } from '@playcanvas/react';
|
|
7
|
-
import { Camera, Script } from '@playcanvas/react/components';
|
|
8
|
-
import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
|
|
9
|
-
|
|
10
|
-
<Entity name="Camera" position={[4, 1, 4]}>
|
|
11
|
-
<Camera />
|
|
12
|
-
<Script script={CameraControls} />
|
|
13
|
-
</Entity>;
|
|
14
|
-
```
|
|
3
|
+
Import the selected class from its installed module. Render `Script` from
|
|
4
|
+
`@playcanvas/react/components` inside the owning `Entity`, passing the class through its `script` prop.
|
|
15
5
|
|
|
16
6
|
Keep imported or custom script constructors at module scope. Declaring a class inside a component
|
|
17
7
|
creates a new identity on render and can recreate the script.
|
|
@@ -1,20 +1,7 @@
|
|
|
1
1
|
# Web Components projects
|
|
2
2
|
|
|
3
|
-
Expose
|
|
4
|
-
|
|
5
|
-
```html
|
|
6
|
-
<pc-app>
|
|
7
|
-
<pc-asset src="/scripts/camera-controls.mjs"></pc-asset>
|
|
8
|
-
<pc-scene>
|
|
9
|
-
<pc-entity name="Camera">
|
|
10
|
-
<pc-camera></pc-camera>
|
|
11
|
-
<pc-script>
|
|
12
|
-
<pc-script-instance name="cameraControls" move-speed="4"></pc-script-instance>
|
|
13
|
-
</pc-script>
|
|
14
|
-
</pc-entity>
|
|
15
|
-
</pc-scene>
|
|
16
|
-
</pc-app>
|
|
17
|
-
```
|
|
3
|
+
Expose the selected script as an ES module and register its URL with a `pc-asset` directly under
|
|
4
|
+
`pc-app`. Attach a `pc-script` to the owning `pc-entity`, with a `pc-script-instance` for the class.
|
|
18
5
|
|
|
19
6
|
The `name` must equal the class's static `scriptName`. Extra kebab-case attributes map to camelCase
|
|
20
7
|
script properties. Use the `attributes` JSON attribute for nested or reserved properties; it
|
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: verify-pixels
|
|
3
|
-
description: Use when changing PlayCanvas rendering code that must not change the rendered image — draw-call optimization, hardware instancing, shader or material refactors, or asset pipeline swaps —
|
|
3
|
+
description: Use when changing PlayCanvas rendering code that must not visibly change the rendered image — draw-call optimization, hardware instancing, shader or material refactors, noise or lightmap bakes, or asset pipeline swaps — before shipping.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Prove pixels unchanged
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
Choose the gate before building the capture harness. Both classes require controlled captures
|
|
9
|
+
that cover the affected surfaces.
|
|
10
|
+
|
|
11
|
+
## Classify the change
|
|
12
|
+
|
|
13
|
+
| Change | Gate |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| Batching, instancing, mesh indexing, chunking, pooling that preserves RNG order, an asset format swap with identical decoded data | Byte-exact matrix |
|
|
16
|
+
| Re-rolled or re-sampled procedural noise, a changed RNG consumption order, math moved between CPU and GPU, changed precision, fewer octaves or samples | Not byte-gateable: side-by-side review |
|
|
17
|
+
|
|
18
|
+
For the second class, sampled values or precision can change. Report it as "not byte-gateable" and
|
|
19
|
+
use the side-by-side gate after the shared capture setup below; do not invent uncalibrated perceptual
|
|
20
|
+
thresholds to approve it.
|
|
21
|
+
|
|
22
|
+
## Cover every touched surface
|
|
23
|
+
|
|
24
|
+
List every touched material or surface and a capture pose it dominates. Use at least two
|
|
25
|
+
representative poses, adding as many as coverage requires; a horizon strip does not cover a water
|
|
26
|
+
shader change. For animated surfaces, include a second phase mid-animation.
|
|
11
27
|
|
|
12
28
|
## Make the frame deterministic
|
|
13
29
|
|
|
30
|
+
For both gates, load assets fully and match the scene state, camera, canvas size, lighting, and
|
|
31
|
+
animation time between builds. Seed incidental randomness so only the intended change varies.
|
|
32
|
+
|
|
14
33
|
- Drive every animated shader or vertex effect from one app-owned time value, never `Date.now()` or
|
|
15
34
|
`performance.now()` read inside the render path, so a captured phase is exactly reproducible.
|
|
16
35
|
- Freeze the clock with `app.timeScale = 0` before capturing; nothing should advance between frames
|
|
@@ -18,33 +37,34 @@ prove the capture harness itself is trustworthy before trusting what it reports
|
|
|
18
37
|
- Step frames explicitly: set `app.autoRender = false` once, then set `app.renderNextFrame = true`
|
|
19
38
|
before each frame you want rendered. The engine renders exactly that frame and clears the flag —
|
|
20
39
|
do not rely on the free-running render loop plus a timed screenshot.
|
|
21
|
-
- Read the rendered pixels with `await device.readPixelsAsync(x, y, w, h, pixels)` against the exact
|
|
22
|
-
backbuffer, not a re-encoded screenshot (e.g. `canvas.toDataURL`) that can introduce compression or
|
|
23
|
-
colour-management differences the eye won't catch. In the installed engine this method lives on
|
|
24
|
-
`WebglGraphicsDevice`, not the base `GraphicsDevice` type, so narrow to it (or branch on
|
|
25
|
-
`device.isWebGL2`) before calling; a WebGPU project needs its own equivalent readback. If the
|
|
26
|
-
project already has a screenshot path, hold it to the same rule: fixed size, fixed pose, no lossy
|
|
27
|
-
step before the byte comparison.
|
|
28
40
|
|
|
29
|
-
##
|
|
41
|
+
## Gate and report the byte-exact class
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
it, with the same canvas size, camera, and lighting each time.
|
|
36
|
-
|
|
37
|
-
## Run a same-build control first
|
|
43
|
+
Read the exact backbuffer with `await device.readPixelsAsync(x, y, w, h, pixels)`. In the installed
|
|
44
|
+
engine this method lives on `WebglGraphicsDevice`, so narrow to it; WebGPU needs an equivalent
|
|
45
|
+
readback. An existing capture path must preserve raw pixels without colour conversion or lossy
|
|
46
|
+
encoding before comparison.
|
|
38
47
|
|
|
39
48
|
Before trusting any diff between the old and new build, capture the same pose × phase matrix twice
|
|
40
49
|
from the *unmodified* build. Two captures of identical, frozen state must be bit-identical. If they
|
|
41
50
|
are not, the capture path itself is the source of noise — an unseeded animation, an asset still
|
|
42
51
|
loading, a GPU timing race — and must be fixed before it can say anything about the real change.
|
|
43
52
|
|
|
44
|
-
## Gate and report
|
|
45
|
-
|
|
46
53
|
Byte-compare each pose × phase pair between the two builds; do not diff by looking. Zero differing
|
|
47
54
|
pixels passes outright. Any nonzero diff must be reviewed on-screen, and its cause and extent stated
|
|
48
55
|
in the change description — never merged silently. Report the actual count every time, for example
|
|
49
56
|
"0 of 65536 pixels differ" or "312 of 65536 pixels differ, confined to the object's silhouette edge".
|
|
50
57
|
"Looks the same" or "no visible difference" is not a result.
|
|
58
|
+
|
|
59
|
+
## Review the other class side by side
|
|
60
|
+
|
|
61
|
+
For each planned pose and phase, show the controlled old and new captures together in one image
|
|
62
|
+
for the user's accept or reject. State what differs and why, for example "noise tile replaces
|
|
63
|
+
runtime fbm: pattern period changed, tint and amplitude match". If no difference is visible, report
|
|
64
|
+
that. Skip the cross-build byte comparison for this class.
|
|
65
|
+
|
|
66
|
+
## Keep the harness small
|
|
67
|
+
|
|
68
|
+
Reuse the project's capture harness when available. Keep harness code out of the shipped bundle,
|
|
69
|
+
behind a dev-only import or in a tools directory, and list its files in the change description.
|
|
70
|
+
Capture with a headless browser and return images only at accept-or-reject points, not after every edit.
|
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
"typecheck": "tsc --noEmit"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"playcanvas": "^2.
|
|
18
|
+
"playcanvas": "^2.22.6"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@playcanvas/eslint-config": "3.0.0-beta.8",
|
|
22
22
|
"eslint": "^9.39.5",
|
|
23
|
-
"prettier": "^3.9.
|
|
23
|
+
"prettier": "^3.9.9",
|
|
24
24
|
"typescript": "~6.0.3",
|
|
25
|
-
"vite": "^8.
|
|
25
|
+
"vite": "^8.3.1"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=22.23.2"
|
|
@@ -7,7 +7,7 @@ import { useState } from 'react';
|
|
|
7
7
|
import { ArPlacement } from './ar-placement';
|
|
8
8
|
import './starter.css';
|
|
9
9
|
|
|
10
|
-
function
|
|
10
|
+
function Scene() {
|
|
11
11
|
const [camera, setCamera] = useState<PcEntity | null>(null);
|
|
12
12
|
const [preview, setPreview] = useState<PcEntity | null>(null);
|
|
13
13
|
const surface = useMaterial({ diffuse: '#474d57' });
|
|
@@ -16,29 +16,37 @@ function App() {
|
|
|
16
16
|
|
|
17
17
|
return (
|
|
18
18
|
<>
|
|
19
|
-
<
|
|
20
|
-
<
|
|
21
|
-
|
|
19
|
+
<Entity name="camera" ref={setCamera} position={[3, 2.4, 4]} rotation={[-20, 37, 0]}>
|
|
20
|
+
<Camera clearColor="#0d141f" />
|
|
21
|
+
</Entity>
|
|
22
|
+
<Entity name="light" rotation={[45, 30, 0]}>
|
|
23
|
+
<Light type="directional" intensity={2} />
|
|
24
|
+
</Entity>
|
|
25
|
+
<Entity name="preview" ref={setPreview}>
|
|
26
|
+
<Entity name="surface" scale={[4, 0.15, 4]}>
|
|
27
|
+
<Render type="cylinder" material={surface} />
|
|
28
|
+
</Entity>
|
|
29
|
+
<Entity name="box" position={[0, 0.65, 0]}>
|
|
30
|
+
<Render type="box" material={cyan} />
|
|
22
31
|
</Entity>
|
|
23
|
-
<Entity name="
|
|
24
|
-
<
|
|
32
|
+
<Entity name="reticle" position={[0, 0.12, 0]} scale={[1.5, 0.03, 1.5]}>
|
|
33
|
+
<Render type="cylinder" material={coral} />
|
|
25
34
|
</Entity>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<Entity name="box" position={[0, 0.65, 0]}>
|
|
31
|
-
<Render type="box" material={cyan} />
|
|
32
|
-
</Entity>
|
|
33
|
-
<Entity name="reticle" position={[0, 0.12, 0]} scale={[1.5, 0.03, 1.5]}>
|
|
34
|
-
<Render type="cylinder" material={coral} />
|
|
35
|
-
</Entity>
|
|
35
|
+
</Entity>
|
|
36
|
+
{camera && preview && (
|
|
37
|
+
<Entity name="placement">
|
|
38
|
+
<Script script={ArPlacement} camera={camera} preview={preview} />
|
|
36
39
|
</Entity>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
)}
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function App() {
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
<Application graphicsDeviceOptions={{ alpha: true, xrCompatible: true }}>
|
|
49
|
+
<Scene />
|
|
42
50
|
</Application>
|
|
43
51
|
<div className="hud">
|
|
44
52
|
<section className="panel">
|
|
@@ -15,20 +15,20 @@
|
|
|
15
15
|
"typecheck": "tsc --noEmit"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@playcanvas/react": "^0.11.
|
|
19
|
-
"playcanvas": "^2.
|
|
20
|
-
"react": "^19.
|
|
21
|
-
"react-dom": "^19.
|
|
18
|
+
"@playcanvas/react": "^0.11.7",
|
|
19
|
+
"playcanvas": "^2.22.6",
|
|
20
|
+
"react": "^19.3.0",
|
|
21
|
+
"react-dom": "^19.3.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@playcanvas/eslint-config": "3.0.0-beta.8",
|
|
25
|
-
"@types/react": "^19.
|
|
26
|
-
"@types/react-dom": "^19.
|
|
27
|
-
"@vitejs/plugin-react": "^6.
|
|
25
|
+
"@types/react": "^19.3.0",
|
|
26
|
+
"@types/react-dom": "^19.3.0",
|
|
27
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
28
28
|
"eslint": "^9.39.5",
|
|
29
|
-
"prettier": "^3.9.
|
|
29
|
+
"prettier": "^3.9.9",
|
|
30
30
|
"typescript": "~6.0.3",
|
|
31
|
-
"vite": "^8.
|
|
31
|
+
"vite": "^8.3.1"
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|
|
34
34
|
"node": ">=22.23.2"
|
|
@@ -5,25 +5,33 @@ import { useMaterial } from '@playcanvas/react/hooks';
|
|
|
5
5
|
import { SpriteGame } from './sprite-game';
|
|
6
6
|
import './starter.css';
|
|
7
7
|
|
|
8
|
-
function
|
|
8
|
+
function Scene() {
|
|
9
9
|
const dirt = useMaterial({ emissive: '#734729' });
|
|
10
10
|
const grass = useMaterial({ emissive: '#479e57' });
|
|
11
11
|
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
<Entity name="camera" position={[0, 0, 10]}>
|
|
15
|
+
<Camera projection={1} orthoHeight={4.4} clearColor="#5cb9e6" />
|
|
16
|
+
</Entity>
|
|
17
|
+
<Entity name="dirt" position={[0, -3.39, 0]} scale={[20, 2.6, 1]}>
|
|
18
|
+
<Render type="box" material={dirt} />
|
|
19
|
+
</Entity>
|
|
20
|
+
<Entity name="grass" position={[0, -2, 0]} scale={[20, 0.18, 1]}>
|
|
21
|
+
<Render type="box" material={grass} />
|
|
22
|
+
</Entity>
|
|
23
|
+
<Entity name="game" position={[0, -2.15, 0]}>
|
|
24
|
+
<Script script={SpriteGame} />
|
|
25
|
+
</Entity>
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function App() {
|
|
12
31
|
return (
|
|
13
32
|
<>
|
|
14
33
|
<Application>
|
|
15
|
-
<
|
|
16
|
-
<Camera projection={1} orthoHeight={4.4} clearColor="#5cb9e6" />
|
|
17
|
-
</Entity>
|
|
18
|
-
<Entity name="dirt" position={[0, -3.39, 0]} scale={[20, 2.6, 1]}>
|
|
19
|
-
<Render type="box" material={dirt} />
|
|
20
|
-
</Entity>
|
|
21
|
-
<Entity name="grass" position={[0, -2, 0]} scale={[20, 0.18, 1]}>
|
|
22
|
-
<Render type="box" material={grass} />
|
|
23
|
-
</Entity>
|
|
24
|
-
<Entity name="game" position={[0, -2.15, 0]}>
|
|
25
|
-
<Script script={SpriteGame} />
|
|
26
|
-
</Entity>
|
|
34
|
+
<Scene />
|
|
27
35
|
</Application>
|
|
28
36
|
<div className="hud">
|
|
29
37
|
<section className="panel">
|
|
@@ -15,15 +15,15 @@
|
|
|
15
15
|
"typecheck": "tsc --noEmit"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@playcanvas/web-components": "^0.
|
|
19
|
-
"playcanvas": "^2.
|
|
18
|
+
"@playcanvas/web-components": "^0.26.0",
|
|
19
|
+
"playcanvas": "^2.22.6"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@playcanvas/eslint-config": "3.0.0-beta.8",
|
|
23
23
|
"eslint": "^9.39.5",
|
|
24
|
-
"prettier": "^3.9.
|
|
24
|
+
"prettier": "^3.9.9",
|
|
25
25
|
"typescript": "~6.0.3",
|
|
26
|
-
"vite": "^8.
|
|
26
|
+
"vite": "^8.3.1"
|
|
27
27
|
},
|
|
28
28
|
"engines": {
|
|
29
29
|
"node": ">=22.23.2"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<body>
|
|
10
10
|
<pc-app>
|
|
11
11
|
<!-- Materials are declared once and referenced by id -->
|
|
12
|
-
<pc-material id="sphere-material" diffuse="
|
|
12
|
+
<pc-material id="sphere-material" diffuse="#e6edf9"></pc-material>
|
|
13
13
|
|
|
14
14
|
<pc-scene>
|
|
15
15
|
<!-- Camera. Orbit controls are attached in src/main.ts -->
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<!-- Antialiasing is disabled - splat rendering is fragment bound, so MSAA costs a lot and adds little -->
|
|
11
|
-
<pc-app antialias="false"
|
|
11
|
+
<pc-app antialias="false" max-pixel-ratio="1">
|
|
12
12
|
<!-- Hosted so the template stays small. Capture or edit your own splat at
|
|
13
13
|
https://superspl.at, then drop the .sog or .ply in public/ and point this at it instead -->
|
|
14
14
|
<pc-asset id="splat" src="https://developer.playcanvas.com/assets/toy-cat.sog"></pc-asset>
|