lecodes-cli 2.4.0 → 2.4.2

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.
Files changed (44) hide show
  1. package/dist/index.js +18930 -1829
  2. package/package.json +10 -9
  3. package/runtime/sdk-types.json +1 -1
  4. package/runtime/web/VERSION.json +3 -3
  5. package/runtime/web/assets/ar-D6oDL4pw.js +1 -0
  6. package/runtime/web/assets/ar-mVXX3kRQ.js +2 -0
  7. package/runtime/web/assets/qrAnchorWorker-Cb5E8cF-.js +1 -0
  8. package/runtime/web/assets/qr_anchor_wasm_bg-D_ePkCCu.wasm +0 -0
  9. package/runtime/web/index.html +1 -0
  10. package/runtime/web/player.js +12 -12
  11. package/runtime/web-lite/assets/{index-By07xWGA.js → index-oPsDD47E.js} +1 -1
  12. package/runtime/web-lite/assets/scene3d-BAl_QQyi.js +1105 -0
  13. package/runtime/web-lite/features.json +2 -2
  14. package/runtime/web-lite/index.html +2 -1
  15. package/runtime/web-lite/lite.js +9 -9
  16. package/runtime/web-lite/plugins/ar/_virtual_lecodes-half-resources-CNYmVR88.js +1 -0
  17. package/runtime/web-lite/plugins/ar/assets/qrAnchorWorker-DcxVKAqo.js +1 -0
  18. package/runtime/web-lite/plugins/ar/assets/qr_anchor_wasm_bg-D_ePkCCu.wasm +0 -0
  19. package/runtime/web-lite/plugins/ar/index-C2uuuLhz.js +2 -0
  20. package/runtime/web-lite/plugins.json +1 -1
  21. package/src/commands/app/index.ts +26 -9
  22. package/src/commands/app/shared.ts +72 -22
  23. package/src/commands/plugin.ts +3 -3
  24. package/src/commands/push.ts +2 -2
  25. package/src/commands/web.ts +12 -5
  26. package/src/compile/collect.ts +2 -3
  27. package/src/compile/collectLocal.ts +13 -2
  28. package/src/compile/designCompile.ts +2 -3
  29. package/src/compile/projectCompile.ts +3 -0
  30. package/src/dev/devServer.ts +63 -7
  31. package/src/dev/https.ts +50 -0
  32. package/src/dev/serve.ts +15 -9
  33. package/src/dev/webRunner.ts +11 -3
  34. package/src/platform/serverDiff.ts +2 -3
  35. package/src/plugin/emitWeb.ts +17 -8
  36. package/src/plugin/firstParty.ts +30 -0
  37. package/src/plugin/generate.ts +41 -21
  38. package/src/plugin/manifest.ts +33 -3
  39. package/src/project/localFiles.ts +44 -7
  40. package/src/web/build.ts +57 -11
  41. package/src/web/halves.ts +76 -0
  42. package/runtime/web/assets/ar-CHvaraG4.js +0 -1
  43. package/runtime/web-lite/assets/scene3d-D5R7Vq3f.js +0 -888
  44. package/runtime/web-lite/plugins/ar/index-CHvaraG4.js +0 -1
@@ -0,0 +1 @@
1
+ const r=""+new URL("assets/qrAnchorWorker-DcxVKAqo.js",import.meta.url).href,o={qrAnchorWorker:r};export{o as resources};
@@ -0,0 +1 @@
1
+ class y{__destroy_into_raw(){const t=this.__wbg_ptr;return this.__wbg_ptr=0,S.unregister(this),t}free(){const t=this.__destroy_into_raw();s.__wbg_qranchor_free(t,0)}clear_hints(){s.qranchor_clear_hints(this.__wbg_ptr)}frame_height(){return s.qranchor_frame_height(this.__wbg_ptr)>>>0}frame_ptr(t,r){return s.qranchor_frame_ptr(this.__wbg_ptr,t,r)>>>0}frame_width(){return s.qranchor_frame_width(this.__wbg_ptr)>>>0}gray_from_rgba(){s.qranchor_gray_from_rgba(this.__wbg_ptr)}constructor(t){const r=R(t,s.__wbindgen_malloc,s.__wbindgen_realloc),n=b,o=s.qranchor_new(r,n);if(o[2])throw k(o[1]);return this.__wbg_ptr=o[0],S.register(this,this.__wbg_ptr,this),this}output_len(){return s.qranchor_output_len(this.__wbg_ptr)>>>0}output_ptr(){return s.qranchor_output_ptr(this.__wbg_ptr)>>>0}process(t,r,n,o){return s.qranchor_process(this.__wbg_ptr,t,r,n,o)>>>0}process_gl(t,r,n,o){return s.qranchor_process_gl(this.__wbg_ptr,t,r,n,o)>>>0}reset(){s.qranchor_reset(this.__wbg_ptr)}rgba_ptr(t,r){return s.qranchor_rgba_ptr(this.__wbg_ptr,t,r)>>>0}set_hint(t,r,n,o,c){s.qranchor_set_hint(this.__wbg_ptr,t,r,n,o,c)}set_targets(t){const r=R(t,s.__wbindgen_malloc,s.__wbindgen_realloc),n=b,o=s.qranchor_set_targets(this.__wbg_ptr,r,n);if(o[2])throw k(o[1]);return o[0]>>>0}}Symbol.dispose&&(y.prototype[Symbol.dispose]=y.prototype.free);function x(){return s.output_layout_version()>>>0}function T(){return s.output_stride()>>>0}function C(){let e,t;try{const r=s.version();return e=r[0],t=r[1],g(r[0],r[1])}finally{s.__wbindgen_free(e,t,1)}}function $(){return{__proto__:null,"./qr_anchor_wasm_bg.js":{__proto__:null,__wbg_Error_30c8987f7c2ed4e2:function(t,r){return Error(g(t,r))},__wbg___wbindgen_throw_41e9ee4f547fc59a:function(t,r){throw new Error(g(t,r))},__wbg_error_757e9472f8410341:function(t,r){let n,o;try{n=t,o=r,console.error(g(t,r))}finally{s.__wbindgen_free(n,o,1)}},__wbg_new_227d7c05414eb861:function(){return new Error},__wbg_stack_3b0d974bbf31e44f:function(t,r){const n=r.stack,o=R(n,s.__wbindgen_malloc,s.__wbindgen_realloc),c=b;M().setInt32(t+4,c,!0),M().setInt32(t+0,o,!0)},__wbindgen_init_externref_table:function(){const t=s.__wbindgen_externrefs,r=t.grow(4);t.set(0,void 0),t.set(r+0,void 0),t.set(r+1,null),t.set(r+2,!0),t.set(r+3,!1)}}}}const S=typeof FinalizationRegistry>"u"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(e=>s.__wbg_qranchor_free(e,1));let f=null;function M(){return(f===null||f.buffer.detached===!0||f.buffer.detached===void 0&&f.buffer!==s.memory.buffer)&&(f=new DataView(s.memory.buffer)),f}function g(e,t){return z(e>>>0,t)}let u=null;function h(){return(u===null||u.byteLength===0)&&(u=new Uint8Array(s.memory.buffer)),u}function R(e,t,r){if(r===void 0){const _=l.encode(e),a=t(_.length,1)>>>0;return h().subarray(a,a+_.length).set(_),b=_.length,a}let n=e.length,o=t(n,1)>>>0;const c=h();let i=0;for(;i<n;i++){const _=e.charCodeAt(i);if(_>127)break;c[o+i]=_}if(i!==n){i!==0&&(e=e.slice(i)),o=r(o,n,n=i+e.length*3,1)>>>0;const _=h().subarray(o+i,o+n),a=l.encodeInto(e,_);i+=a.written,o=r(o,n,i,1)>>>0}return b=i,o}function k(e){const t=s.__wbindgen_externrefs.get(e);return s.__externref_table_dealloc(e),t}let d=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});d.decode();const j=2146435072;let E=0;function z(e,t){return E+=t,E>=j&&(d=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),d.decode(),E=t),d.decode(h().subarray(e,e+t))}const l=new TextEncoder;"encodeInto"in l||(l.encodeInto=function(e,t){const r=l.encode(e);return t.set(r),{read:e.length,written:r.length}});let b=0,s;function B(e,t){return s=e.exports,f=null,u=null,s.__wbindgen_start(),s}async function N(e,t){if(typeof Response=="function"&&e instanceof Response){if(!e.ok)throw new Error(`failed to fetch Wasm: ${e.status} ${e.statusText} fetching '${e.url}'`);if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(e,t)}catch(o){if(r(e.type)&&e.headers.get("Content-Type")!=="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",o);else throw o}const n=await e.arrayBuffer();return await WebAssembly.instantiate(n,t)}else{const n=await WebAssembly.instantiate(e,t);return n instanceof WebAssembly.Instance?{instance:n,module:e}:n}function r(n){switch(n){case"basic":case"cors":case"default":return!0}return!1}}async function P(e){if(s!==void 0)return s;e!==void 0&&(Object.getPrototypeOf(e)===Object.prototype?{module_or_path:e}=e:console.warn("using deprecated parameters for the initialization function; pass a single object instead")),e===void 0&&(e=new URL(""+new URL("qr_anchor_wasm_bg-D_ePkCCu.wasm",import.meta.url).href,import.meta.url));const t=$();(typeof e=="string"||typeof Request=="function"&&e instanceof Request||typeof URL=="function"&&e instanceof URL)&&(e=fetch(e));const{instance:r,module:n}=await N(await e,t);return B(r)}const v=1,D=28,m=self;let p=null;const w={};let O=!1;const U=e=>{var r,n;const t=JSON.stringify(e);(r=w.seeking)==null||r.set_targets(t),(n=w.holding)==null||n.set_targets(t)},V=async e=>{if(e.type==="init"){const q=performance.now(),A=await P({module_or_path:new URL(""+new URL("qr_anchor_wasm_bg-D_ePkCCu.wasm",import.meta.url).href,import.meta.url)});if(x()!==v||T()!==D)throw new Error(`its output is layout ${x()} × ${T()}, this worker reads ${v} × ${D}`);p=A.memory,w.seeking=new y(JSON.stringify(e.config.seeking)),w.holding=new y(JSON.stringify(e.config.holding)),U(e.targets),m.postMessage({type:"ready",version:C(),ms:performance.now()-q});return}if(e.type==="targets"){U(e.targets);return}const t=w[e.phase];if(!t||!p)throw new Error("a frame before the detector was made");const{seq:r,width:n,height:o}=e,c=performance.now(),i=t.frame_ptr(n,o);let _=0;if(i){new Uint8Array(p.buffer,i,n*o).set(new Uint8Array(e.gray,0,n*o));for(const[q,A,F,I,L]of e.hints)t.set_hint(q,A,F,I,L);_=t.process_gl(e.p0,e.p5,e.p8,e.p9)}const a=_>0?new Float64Array(p.buffer,t.output_ptr(),t.output_len()).slice():new Float64Array(0);m.postMessage({type:"result",seq:r,observations:a,processMs:performance.now()-c},[a.buffer])};let W=Promise.resolve();m.onmessage=e=>{const t=e.data;W=W.then(()=>{if(!O)return V(t).catch(r=>{O=!0,m.postMessage({type:"failed",message:r instanceof Error?r.message:String(r)})})})};
@@ -0,0 +1,2 @@
1
+ var tt=Object.defineProperty;var nt=(e,t,n)=>t in e?tt(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var ue=(e,t,n)=>nt(e,typeof t!="symbol"?t+"":t,n);const rt="modulepreload",ot=function(e,t){return new URL(e,t).href},Ie={},st=function(t,n,r){let s=Promise.resolve();if(n&&n.length>0){let f=function(d){return Promise.all(d.map(p=>Promise.resolve(p).then(v=>({status:"fulfilled",value:v}),v=>({status:"rejected",reason:v}))))};const h=document.getElementsByTagName("link"),c=document.querySelector("meta[property=csp-nonce]"),m=(c==null?void 0:c.nonce)||(c==null?void 0:c.getAttribute("nonce"));s=f(n.map(d=>{if(d=ot(d,r),d in Ie)return;Ie[d]=!0;const p=d.endsWith(".css"),v=p?'[rel="stylesheet"]':"";if(r)for(let b=h.length-1;b>=0;b--){const x=h[b];if(x.href===d&&(!p||x.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${d}"]${v}`))return;const g=document.createElement("link");if(g.rel=p?"stylesheet":rt,p||(g.as="script"),g.crossOrigin="",g.href=d,m&&g.setAttribute("nonce",m),document.head.appendChild(g),p)return new Promise((b,x)=>{g.addEventListener("load",b),g.addEventListener("error",()=>x(Error(`Unable to preload CSS for ${d}`)))})}))}function i(f){const h=new Event("vite:preloadError",{cancelable:!0});if(h.payload=f,window.dispatchEvent(h),!h.defaultPrevented)throw f}return s.then(f=>{for(const h of f||[])h.status==="rejected"&&i(h.reason);return t().catch(i)})},it=(e,t)=>{const n=t.data instanceof Uint8Array?t.data.slice().buffer:t.data;return{systemId:e.putBuffer(n),name:t.name,size:n.byteLength}},S={present:e=>e!=null,f64:e=>typeof e=="number"&&Number.isFinite(e)?e:void 0,i32:e=>typeof e=="number"&&Number.isInteger(e)&&e>=-2147483648&&e<=2147483647?e:void 0,bool:e=>typeof e=="boolean"?e:void 0,string:e=>typeof e=="string"?e:void 0,json:e=>e,bytes:e=>Object.prototype.toString.call(e)==="[object Uint8Array]"?e.slice():void 0,object:e=>typeof e=="object"&&e!==null&&!Array.isArray(e)?e:void 0,array:(e,t)=>Array.isArray(e)&&(t===void 0||e.length===t)?e:void 0,list:(e,t)=>{if(!Array.isArray(e))return;const n=[];for(const r of e){const s=t(r);if(s===void 0)return;n.push(s)}return n},map:(e,t)=>{if(typeof e!="object"||e===null||Array.isArray(e))return;const n={};for(const[r,s]of Object.entries(e)){const i=t(s);if(i===void 0)return;n[r]=i}return n}};let xe;const at=e=>{const t=S.object(e),n=t?S.i32(t.systemId):void 0,r=n===void 0||xe==null?void 0:xe(n);return t&&r?{data:r,name:S.string(t.name)??""}:void 0},he=e=>typeof e=="number"&&Number.isInteger(e)&&e>=0&&e<=4294967295?e:void 0,ct=e=>{const t=S.object(e);if(!t)return;let n;if(S.present(t.scene)){const i=he(t.scene);if(i===void 0)return;n=i}let r;if(S.present(t.camera)){const i=he(t.camera);if(i===void 0)return;r=i}const s={};return n!==void 0&&(s.scene=n),r!==void 0&&(s.camera=r),s},lt=e=>e==="world"||e==="images"?e:void 0,dt=e=>e==="horizontal"||e==="vertical"||e==="both"||e==="none"?e:void 0,Qe=e=>{const t=S.object(e);if(!t)return;const n=at(t.image);if(n===void 0)return;const r=S.f64(t.physicalWidth);if(r===void 0)return;const s=he(t.entity);return s===void 0?void 0:{image:n,physicalWidth:r,entity:s}},Ye=e=>{const t=S.object(e);if(!t)return;const n=S.string(t.text);if(n===void 0)return;const r=S.f64(t.size);if(r===void 0)return;const s=he(t.entity);return s===void 0?void 0:{text:n,size:r,entity:s}},ut=e=>e==="gravity"||e==="gravityAndHeading"?e:void 0,ft=e=>{const t=S.object(e);if(!t)return;let n;if(S.present(t.mode)){const d=lt(t.mode);if(d===void 0)return;n=d}let r;if(S.present(t.planes)){const d=dt(t.planes);if(d===void 0)return;r=d}let s;if(S.present(t.images)){const d=S.list(t.images,p=>Qe(p));if(d===void 0)return;s=d}let i;if(S.present(t.codes)){const d=S.list(t.codes,p=>Ye(p));if(d===void 0)return;i=d}let f;if(S.present(t.worldAlignment)){const d=ut(t.worldAlignment);if(d===void 0)return;f=d}let h;if(S.present(t.lightEstimation)){const d=S.bool(t.lightEstimation);if(d===void 0)return;h=d}let c;if(S.present(t.autoFocus)){const d=S.bool(t.autoFocus);if(d===void 0)return;c=d}const m={};return n!==void 0&&(m.mode=n),r!==void 0&&(m.planes=r),s!==void 0&&(m.images=s),i!==void 0&&(m.codes=i),f!==void 0&&(m.worldAlignment=f),h!==void 0&&(m.lightEstimation=h),c!==void 0&&(m.autoFocus=c),m},ht=e=>e==="planes"||e==="estimated"||e==="any"?e:void 0,mt=e=>{const t=S.object(e);if(!t)return;const n=S.f64(t.x);if(n===void 0)return;const r=S.f64(t.y);if(r===void 0)return;const s=S.f64(t.z);return s===void 0?void 0:{x:n,y:r,z:s}},wt=e=>{const t=S.object(e);if(!t)return;const n=S.f64(t.x);if(n===void 0)return;const r=S.f64(t.y);if(r===void 0)return;const s=S.f64(t.z);if(s===void 0)return;const i=S.f64(t.w);return i===void 0?void 0:{x:n,y:r,z:s,w:i}},gt=e=>{const t=S.object(e);if(!t)return;const n=mt(t.position);if(n===void 0)return;const r=wt(t.rotation);return r===void 0?void 0:{position:n,rotation:r}};class z extends Error{constructor(t){super(t),this.code=t}}const Le={name:"ar",version:2},yt=(e,t)=>{xe=n=>e.getBuffer(n),e.registerService(Le.name,(n,r)=>{const s={tracking:h=>r.emit("tracking",h),plane:h=>r.emit("plane",h),image:h=>r.emit("image",h),anchor:h=>r.emit("anchor",h),light:h=>r.emit("light",h),error:h=>r.emit("error",h),ended:h=>r.emit("ended",h)},i=t(ct(n)??{},s);return{call:async(h,c)=>{switch(h){case"start":{let m;if(S.present(c[0])){const d=ft(c[0]);if(d===void 0)throw new Error("bad arguments — AR.start: options");m=d}await i.start(m);return}case"stop":{await i.stop();return}case"setImages":{const m=S.list(c[0],d=>Qe(d));if(m===void 0)throw new Error("bad arguments — AR.setImages: images");await i.setImages(m);return}case"setCodes":{const m=S.list(c[0],d=>Ye(d));if(m===void 0)throw new Error("bad arguments — AR.setCodes: codes");await i.setCodes(m);return}case"hitTest":{const m=S.f64(c[0]);if(m===void 0)throw new Error("bad arguments — AR.hitTest: x");const d=S.f64(c[1]);if(d===void 0)throw new Error("bad arguments — AR.hitTest: y");let p;if(S.present(c[2])){const v=ht(c[2]);if(v===void 0)throw new Error("bad arguments — AR.hitTest: target");p=v}return await i.hitTest(m,d,p)}case"anchor":{const m=he(c[0]);if(m===void 0)throw new Error("bad arguments — AR.anchor: entity");const d=gt(c[1]);if(d===void 0)throw new Error("bad arguments — AR.anchor: pose");await i.anchor(m,d);return}case"release":{const m=he(c[0]);if(m===void 0)throw new Error("bad arguments — AR.release: entity");await i.release(m);return}case"captureFrame":{const m=await i.captureFrame();return it(e,m)}default:throw new Error(`Unknown method: ${h}`)}},close:()=>{var h;return(h=i.close)==null?void 0:h.call(i)}}},Le.version)},ce=Math.PI/180,ge=(e,t)=>[e[3]*t[0]+e[0]*t[3]+e[1]*t[2]-e[2]*t[1],e[3]*t[1]-e[0]*t[2]+e[1]*t[3]+e[2]*t[0],e[3]*t[2]+e[0]*t[1]-e[1]*t[0]+e[2]*t[3],e[3]*t[3]-e[0]*t[0]-e[1]*t[1]-e[2]*t[2]],Re=e=>{const t=Math.hypot(e[0],e[1],e[2],e[3])||1;return[e[0]/t,e[1]/t,e[2]/t,e[3]/t]},fe=(e,t)=>{const n=Math.sin(t/2);return[e[0]*n,e[1]*n,e[2]*n,Math.cos(t/2)]},Pe=(e,t)=>{const[n,r,s,i]=e,f=2*(r*t[2]-s*t[1]),h=2*(s*t[0]-n*t[2]),c=2*(n*t[1]-r*t[0]);return[t[0]+i*f+(r*c-s*h),t[1]+i*h+(s*f-n*c),t[2]+i*c+(n*h-r*f)]},_e=(e,t,n)=>ge(ge(fe([0,0,1],e*ce),fe([1,0,0],t*ce)),fe([0,1,0],n*ce)),Ze=e=>{const[t,n,r,s]=e;return[2*(t*r-s*n),2*(s*t+n*r),s*s-t*t-n*n+r*r]},pt=(e,t)=>{const n=e[0]*t[0]+e[1]*t[1]+e[2]*t[2];if(n<-.999999)return[1,0,0,0];const r=[e[1]*t[2]-e[2]*t[1],e[2]*t[0]-e[0]*t[2],e[0]*t[1]-e[1]*t[0]];return Re([r[0],r[1],r[2],1+n])};class Oe{constructor(t=1,n=.05){ue(this,"q",[0,0,0,1]);ue(this,"seeded",!1);ue(this,"ix",0);ue(this,"iy",0);ue(this,"iz",0);this.kp=t,this.ki=n}get ready(){return this.seeded}seed(t){this.q=Re(t),this.seeded=!0}update(t,n,r){const s=Math.hypot(n[0],n[1],n[2]);if(!this.seeded){if(s===0)return;this.seed(pt([n[0]/s,n[1]/s,n[2]/s],[0,0,1]));return}let i=t[0],f=t[1],h=t[2];if(s>0){const x=n[0]/s,R=n[1]/s,$=n[2]/s,M=Ze(this.q),_=R*M[2]-$*M[1],j=$*M[0]-x*M[2],N=x*M[1]-R*M[0];this.ki>0&&(this.ix+=this.ki*_*r,this.iy+=this.ki*j*r,this.iz+=this.ki*N*r,i+=this.ix,f+=this.iy,h+=this.iz),i+=this.kp*_,f+=this.kp*j,h+=this.kp*N}const c=.5*r*i,m=.5*r*f,d=.5*r*h,[p,v,g,b]=this.q;this.q=Re([p+b*c+v*d-g*m,v+b*m-p*d+g*c,g+b*d+p*m-v*c,b-p*c-v*m-g*d])}}const xt=fe([1,0,0],-Math.PI/2),vt=(e,t)=>ge(ge(xt,e),fe([0,0,1],-t*ce)),Et=e=>{let[t,,n]=Pe(e,[0,0,-1]);return Math.hypot(t,n)<.3&&([t,,n]=Pe(e,[0,1,0])),fe([0,1,0],-Math.atan2(-t,-n))},kt=e=>{const[t,n,r,s]=e;return[1-2*(n*n+r*r),2*(t*n+s*r),2*(t*r-s*n),0,2*(t*n-s*r),1-2*(t*t+r*r),2*(n*r+s*t),0,2*(t*r+s*n),2*(n*r-s*t),1-2*(t*t+n*n),0,0,0,0,1]},bt=(e,t,n,r,s)=>{const i=Math.max(e,t)/2/Math.tan(s*ce/2),f=Math.max(n/e,r/t);return 2*Math.atan(r/f/2/i)/ce},At=(e,t,n,r)=>{const s=1/Math.tan(e*ce/2);return[s/t,0,0,0,0,s,0,0,0,0,(r+n)/(n-r),-1,0,0,2*r*n/(n-r),0]},Rt=(e,t,n,r)=>{const s=Math.max(n/e,r/t),i=n/(e*s),f=r/(t*s);return[i,0,(1-i)/2,0,f,(1-f)/2]},Mt=68,Te=.01,je=1e3,Ee=Math.PI/180,qe=30,zt=(e,t,n)=>{const r=t.scene;let s=0,i=!1,f=!1,h=null,c=null,m=null,d="",p=new Oe,v=!1,g=null,b=null,x=0,R=0,$=0,M=null,_=null;const j=k=>{k.alpha===null||k.beta===null||k.gamma===null||k.alpha===0&&k.beta===0&&k.gamma===0||(b=[k.beta,k.gamma],g=_e(k.alpha,k.beta,k.gamma))},N=k=>{const I=k.rotationRate,F=k.accelerationIncludingGravity;if(!I||!F||I.alpha===null||I.beta===null||I.gamma===null||F.x===null||F.y===null||F.z===null)return;const L=k.timeStamp;if(R||(R=L),$===0)if(b){const K=Ze(_e(0,b[0],b[1])),te=Math.hypot(F.x,F.y,F.z)||1;$=(K[0]*F.x+K[1]*F.y+K[2]*F.z)/te<0?-1:1}else if(L-R>500)$=1;else return;const G=x?Math.min(.1,Math.max(0,(L-x)/1e3)):0;x=L,v||(v=!0,g&&p.seed(g)),p.update([I.beta*Ee,I.gamma*Ee,I.alpha*Ee],[F.x*$,F.y*$,F.z*$],G)},V=()=>{if(!c||r===void 0)return;const k=e.viewport(r),{width:I,height:F}=k,L=`${c.videoWidth}x${c.videoHeight}:${I}x${F}`;if(L!==d&&c.videoWidth>0&&I>0&&F>0){d=L;const te=bt(c.videoWidth,c.videoHeight,I,F,Mt);e.setProjection(r,At(te,I/F,Te,je),Te,je),e.setBackground(r,c,Rt(c.videoWidth,c.videoHeight,I,F))}const G=v?p.ready?p.q:null:g;if(!G)return;const K=vt(G,k.rotation);M??(M=Et(K)),e.setCameraMatrix(r,kt(ge(M,K)))},H=async()=>{const k=[window.DeviceMotionEvent,window.DeviceOrientationEvent];if(!k[1])return!1;let I=!0;for(const F of k)if(typeof(F==null?void 0:F.requestPermission)=="function")try{I=await F.requestPermission()==="granted"&&I}catch{I=!1}return I},Y=()=>{_&&window.removeEventListener("pointerup",_,!0),_=null},X=()=>{_||(_=()=>{Y(),H().then(k=>{k||console.warn("[ar] the page was not given the device's sensors: the camera of the scene stays as the app turned it")})},window.addEventListener("pointerup",_,!0))},J=()=>{s++,i=!1,f=!1,m==null||m(),m=null;for(const k of(h==null?void 0:h.getTracks())??[])k.stop();h=null,c&&(c.srcObject=null,r!==void 0&&(e.setBackground(r,null),e.clearProjection(r)),c.remove()),c=null,d="",Y(),window.removeEventListener("deviceorientation",j),window.removeEventListener("devicemotion",N),p=new Oe,v=!1,g=null,b=null,x=0,R=0,$=0,M=null};return{async start(k){var K,te;if(i||f)throw new z("busy");if(r===void 0||t.camera===void 0)throw console.warn("[ar] start: the session has no scene or camera to drive (ARScene opens it with both)"),new z("failed");((k==null?void 0:k.mode)==="images"||(K=k==null?void 0:k.images)!=null&&K.length)&&console.warn("[ar] the web finds no images in the camera's picture: their anchors are never tracked"),(te=k==null?void 0:k.codes)!=null&&te.length&&console.warn("[ar] the web finds no codes in the camera's picture yet: their anchors are never tracked"),f=!0;const I=++s,F=H();let L;try{L=await navigator.mediaDevices.getUserMedia({audio:!1,video:{facingMode:{ideal:"environment"},width:{ideal:1280},height:{ideal:720}}})}catch(Z){I===s&&(f=!1);const w=Z instanceof DOMException?Z.name:"";throw console.warn(`[ar] no camera: ${Z instanceof Error?Z.message:Z}`),new z(w==="NotAllowedError"||w==="SecurityError"?"denied":w==="NotFoundError"||w==="OverconstrainedError"?"unavailable":"failed")}if(I!==s){for(const Z of L.getTracks())Z.stop();throw new z("failed")}h=L;const G=document.createElement("video");if(G.muted=!0,G.playsInline=!0,G.autoplay=!0,G.srcObject=L,c=G,await new Promise(Z=>{if(G.readyState>=2)return Z();G.addEventListener("loadeddata",()=>Z(),{once:!0})}),await G.play().catch(()=>{}),I!==s)throw new z("failed");window.addEventListener("deviceorientation",j),window.addEventListener("devicemotion",N),m=e.onFrame(V),V(),f=!1,i=!0,n.tracking({state:"normal"}),F.then(Z=>{!Z&&I===s&&X()})},stop(){J()},setImages(k){if(!i)throw new z("notRunning");k.length&&console.warn("[ar] the web finds no images in the camera's picture: their anchors are never tracked")},setCodes(k){if(!i)throw new z("notRunning");k.length&&console.warn("[ar] the web finds no codes in the camera's picture yet: their anchors are never tracked")},hitTest(){if(!i)throw new z("notRunning");return[]},anchor(){if(!i)throw new z("notRunning")},release(){},async captureFrame(){const k=c;if(!i||!k)throw new z("notRunning");const I=s;for(let K=0;k.readyState<2||k.videoWidth===0;K++){if(K>=qe)throw console.warn(`[ar] captureFrame: the camera gave no picture in ${qe} frames`),new z("failed");if(await new Promise(te=>requestAnimationFrame(te)),I!==s)throw new z("notRunning")}const F=document.createElement("canvas");F.width=k.videoWidth,F.height=k.videoHeight;const L=F.getContext("2d");if(!L)throw new z("failed");L.drawImage(k,0,0);const G=await new Promise(K=>F.toBlob(K,"image/jpeg",.9));if(!G)throw new z("failed");return{data:await G.arrayBuffer(),name:"frame.jpg"}},close(){J()}}},$t=180/Math.PI,Me=({position:e,rotation:t})=>{const{x:n,y:r,z:s,w:i}=t;return[1-2*(r*r+s*s),2*(n*r+i*s),2*(n*s-i*r),0,2*(n*r-i*s),1-2*(n*n+s*s),2*(r*s+i*n),0,2*(n*s+i*r),2*(r*s-i*n),1-2*(n*n+r*r),0,e.x,e.y,e.z,1]},ze=e=>{const[t,n,r,s,i,f,h,c,m,d,p,v,g,b,x,R]=Array.from(e),$=t*f-n*i,M=t*h-r*i,_=t*c-s*i,j=n*h-r*f,N=n*c-s*f,V=r*c-s*h,H=m*b-d*g,Y=m*x-p*g,X=m*R-v*g,J=d*x-p*b,k=d*R-v*b,I=p*R-v*x,F=$*I-M*k+_*J+j*X-N*Y+V*H;if(!F)return null;const L=1/F;return[(f*I-h*k+c*J)*L,(r*k-n*I-s*J)*L,(b*V-x*N+R*j)*L,(p*N-d*V-v*j)*L,(h*X-i*I-c*Y)*L,(t*I-r*X+s*Y)*L,(x*_-g*V-R*M)*L,(m*V-p*_+v*M)*L,(i*k-f*X+c*H)*L,(n*X-t*k-s*H)*L,(g*N-b*_+R*$)*L,(d*_-m*N-v*$)*L,(f*Y-i*J-h*H)*L,(t*J-n*Y+r*H)*L,(b*M-g*j-x*$)*L,(m*j-d*M+p*$)*L]},Ft=(e,t)=>{const n=new Array(16);for(let r=0;r<4;r++)for(let s=0;s<4;s++)n[r*4+s]=e[s]*t[r*4]+e[4+s]*t[r*4+1]+e[8+s]*t[r*4+2]+e[12+s]*t[r*4+3];return n},St=e=>{const t=e[0],n=e[1],r=e[2],s=e[4],i=e[5],f=e[6],h=e[8],c=e[9],m=e[10],d=t+i+m;let p,v,g,b;if(d>0){const R=Math.sqrt(d+1)*2;b=R/4,p=(f-c)/R,v=(h-r)/R,g=(n-s)/R}else if(t>i&&t>m){const R=Math.sqrt(1+t-i-m)*2;b=(f-c)/R,p=R/4,v=(s+n)/R,g=(h+r)/R}else if(i>m){const R=Math.sqrt(1+i-t-m)*2;b=(h-r)/R,p=(s+n)/R,v=R/4,g=(c+f)/R}else{const R=Math.sqrt(1+m-t-i)*2;b=(n-s)/R,p=(h+r)/R,v=(c+f)/R,g=R/4}const x=Math.hypot(p,v,g,b)||1;return{x:p/x,y:v/x,z:g/x,w:b/x}},It=e=>({position:{x:e[12],y:e[13],z:e[14]},rotation:St(e)}),Ce=(e,t)=>Math.hypot(e.x-t.x,e.y-t.y,e.z-t.z),De=(e,t)=>{const n=Math.min(1,Math.abs(e.x*t.x+e.y*t.y+e.z*t.z+e.w*t.w));return 2*Math.acos(n)*$t},Lt=(e,t,n)=>{let r=e.x*t.x+e.y*t.y+e.z*t.z+e.w*t.w;const s=r<0?-1:1;r*=s;let i,f;if(r>.9995)i=1-n,f=n*s;else{const v=Math.acos(r),g=Math.sin(v);i=Math.sin((1-n)*v)/g,f=Math.sin(n*v)/g*s}const h=i*e.x+f*t.x,c=i*e.y+f*t.y,m=i*e.z+f*t.z,d=i*e.w+f*t.w,p=Math.hypot(h,c,m,d)||1;return{x:h/p,y:c/p,z:m/p,w:d/p}},Pt=(e,t,n)=>({position:{x:e.position.x+(t.position.x-e.position.x)*n,y:e.position.y+(t.position.y-e.position.y)*n,z:e.position.z+(t.position.z-e.position.z)*n},rotation:Lt(e.rotation,t.rotation,n)}),_t=(e,t,n,r,s)=>{const i=n,f=t,h=i[0]*e.x+i[4]*e.y+i[8]*e.z+i[12],c=i[1]*e.x+i[5]*e.y+i[9]*e.z+i[13],m=i[2]*e.x+i[6]*e.y+i[10]*e.z+i[14],d=i[3]*e.x+i[7]*e.y+i[11]*e.z+i[15],p=f[0]*h+f[4]*c+f[8]*m+f[12]*d,v=f[1]*h+f[5]*c+f[9]*m+f[13]*d,g=f[3]*h+f[7]*c+f[11]*m+f[15]*d;return g>0?[(p/g+1)*r/2,(1-v/g)*s/2]:null},Ot=(e,t,n)=>{const r=n,s=t,i=r[0]*e.x+r[4]*e.y+r[8]*e.z+r[12],f=r[1]*e.x+r[5]*e.y+r[9]*e.z+r[13],h=r[2]*e.x+r[6]*e.y+r[10]*e.z+r[14],c=r[3]*e.x+r[7]*e.y+r[11]*e.z+r[15],m=s[0]*i+s[4]*f+s[8]*h+s[12]*c,d=s[1]*i+s[5]*f+s[9]*h+s[13]*c,p=s[3]*i+s[7]*f+s[11]*h+s[15]*c;return p>0&&Math.abs(m)<=p&&Math.abs(d)<=p},Se={window:24,minObservations:6,minBaseline:.04,minSpread:.01,min:.8,max:1.25,maxResidual:.008,step:.25},Tt=(e,t)=>({p:{x:e[12],y:e[13],z:e[14]},u:{x:e[0]*t.x+e[4]*t.y+e[8]*t.z,y:e[1]*t.x+e[5]*t.y+e[9]*t.z,z:e[2]*t.x+e[6]*t.y+e[10]*t.z}}),jt=(e,t=Se)=>{const n=e.length;if(n<Math.max(2,t.minObservations))return null;let r=0;for(let $=0;$<n;$++)for(let M=$+1;M<n;M++){const _=e[$].p,j=e[M].p;r=Math.max(r,Math.hypot(_.x-j.x,_.y-j.y,_.z-j.z))}if(!(r>=t.minBaseline))return null;let s=0,i=0,f=0,h=0,c=0,m=0;for(const{p:$,u:M}of e)s+=$.x,i+=$.y,f+=$.z,h+=M.x,c+=M.y,m+=M.z;s/=n,i/=n,f/=n,h/=n,c/=n,m/=n;let d=0,p=0;for(const{p:$,u:M}of e){const _=M.x-h,j=M.y-c,N=M.z-m;d+=_*($.x-s)+j*($.y-i)+N*($.z-f),p+=_*_+j*j+N*N}const v=Math.sqrt(p/n);if(!(v>=t.minSpread))return null;const g=-d/p,b=Math.min(t.max,Math.max(t.min,g)),x={x:s+b*h,y:i+b*c,z:f+b*m};let R=0;for(const{p:$,u:M}of e){const _=b*M.x+$.x-x.x,j=b*M.y+$.y-x.y,N=b*M.z+$.z-x.z;R+=_*_+j*j+N*N}return{scale:b,unclamped:g,point:x,residual:Math.sqrt(R/n),baseline:r,spread:v,n}},qt=(e=Se)=>{const t=[];let n=1,r=null;return{get scale(){return n},get estimate(){return r},add(s){t.push(s),t.length>e.window&&t.splice(0,t.length-e.window);const i=jt(t,e);return!i||!(i.residual<=e.maxResidual)?{fit:i,taken:!1}:(n+=(r===null?1:e.step)*(i.scale-n),r=i.scale,{fit:i,taken:!0})},reset(){t.length=0,n=1,r=null}}},ve={min_init_side_px:40,min_reacquire_side_px:20,max_rms_px:.6},Ct={seeking:{...ve,search_first:!1},holding:{...ve,search_first:!0,full_detect_interval:1e6}},We=480,ke=960,be=6,Ne=16,Dt=24,Wt=2,Nt=15,Ht=5,Ut=.02,Gt=2,Bt=.01,Vt=1,Xt=.03,Kt=5,$e=.8,He=10,Ue=30,Qt=.5,Yt=500,Ae=2e3,Zt=5e3,Jt=.005,pe=28,en=(e,t,n=ve.max_rms_px)=>e.rms<=n?e.metres>Xt||e.degrees>Kt?e.inliers!==void 0&&(e.inliers<Dt||e.side<$e*t)?"kept":"move":e.metres<=Bt&&e.degrees<=Vt?"near":e.side>=$e*t?"replace":"kept":"bad",Ge=e=>({position:{x:e.position.x,y:e.position.y,z:e.position.z},rotation:{x:e.orientation.x,y:e.orientation.y,z:e.orientation.z,w:e.orientation.w}});let Fe=!1;const ae=e=>{if(!Fe)try{e==null||e.delete()}catch{}},Be=e=>e instanceof Error?e.message:String(e),tn=e=>{const t=new URL(e,location.href);if(t.origin===location.origin)return{worker:new Worker(t,{type:"module"}),blob:null};const n=URL.createObjectURL(new Blob([`import ${JSON.stringify(t.href)}
2
+ `],{type:"text/javascript"}));try{return{worker:new Worker(n,{type:"module"}),blob:n}}catch(r){throw URL.revokeObjectURL(n),r}},nn=e=>{let t=[],n=0,r=null,s=null,i=!1,f=!1,h=!1,c=null,m=0,d=!1,p=0,v=0,g=null;const b=new Set,x={phase:"seeking",since:0,captures:0,found:0,workerMs:0,captureFrames:0,resultFrames:0},R=o=>{x.since=o,x.captures=x.found=x.workerMs=x.captureFrames=x.resultFrames=0},$=o=>[o.target.size,o.target.size],M=(o,l,a)=>{o.tracked!==l&&(o.tracked=l,console.log(`[ar] qr ${o.target.text}: ${a}`),e.image({entity:o.target.entity,tracked:l,extent:$(o)}))},_=()=>t.map(o=>({text:o.target.text,size:o.target.size})),j=(o,l=[])=>r==null?void 0:r.postMessage(o,l),N=()=>{s&&URL.revokeObjectURL(s),s=null},V=()=>{r==null||r.terminate(),r=null,N(),i=f=d=!1,c=null},H=o=>{h||(h=!0,V(),console.warn(`[ar] the QR detector failed (${o}): QR codes are not tracked`))},Y=()=>{if(r||f||h||t.length===0)return;if(typeof Worker>"u"){H("this browser has no workers");return}f=!0;const o=n;e.workerUrl().then(l=>{if(o!==n)return;if(f=!1,!l)throw new Error("the half was built without its worker (the manifest's qrAnchorWorker)");const a=tn(l),u=a.worker;r=u,s=a.blob,u.onmessage=y=>{r===u&&te(y.data)},u.onerror=y=>{y.preventDefault(),r===u&&H(y.message||"its module did not load")},j({type:"init",config:Ct,targets:_()})}).catch(l=>{o===n&&H(Be(l))})},X=o=>({target:o,anchor:null,anchorPose:null,anchorTracked:!1,asked:null,next:null,nextFrames:0,shown:null,tracked:!1,slew:null,corners:null,cornersIn:[0,0],side:0,misses:0,bestSide:0,loggedAt:-1/0,keptLoggedAt:-1/0,depth:qt(),depthLogged:null,depthRefusedAt:-1/0}),J=(o,l)=>{ae(o.anchor),ae(o.next),o.anchor=o.next=null,o.asked=null,o.depth.reset(),o.depthLogged=null,l&&M(o,!1,"no longer a target")},k=(o,l,a,u)=>{const{fit:y,taken:E}=o.depth.add(Tt(l,a));if(!y)return;const P=y.scale!==y.unclamped?`, clamped from ${y.unclamped.toFixed(3)}`:"",q=`baseline ${(y.baseline*100).toFixed(1)} cm, residual ${(y.residual*1e3).toFixed(1)} mm, ${y.n} obs${P}`;if(E){if(o.depthLogged!==null&&Math.abs(y.scale-o.depthLogged)<=Jt*o.depthLogged)return;o.depthLogged=y.scale,console.log(`[ar] qr ${o.target.text}: depth scale ${o.depth.scale.toFixed(3)} (estimate ${y.scale.toFixed(3)}, ${q})`)}else u-o.depthRefusedAt>=Ae&&(o.depthRefusedAt=u,console.log(`[ar] qr ${o.target.text}: depth scale estimate ${y.scale.toFixed(3)} refused (${q}; the residual over ${(Se.maxResidual*1e3).toFixed(0)} mm), the scale stays ${o.depth.scale.toFixed(3)}`))},I=o=>o.asked||o.next?"waiting":o.anchor&&o.anchorTracked?"holding":"seeking",F=(o,l)=>{const a=n,u={pose:l,wanted:()=>a===n&&o.asked===u&&t.includes(o),settle:y=>{if(o.asked!==u){ae(y);return}o.asked=null,y&&(ae(o.next),o.next=y,o.nextFrames=0)}};o.asked=u,e.anchor(u)},L=(o,l,a)=>{try{if(o.trackedAnchors&&!o.trackedAnchors.has(a))return null;const u=o.getPose(a.anchorSpace,l);return u?Ge(u.transform):null}catch{return null}},G=(o,l)=>{for(const a of t){if(a.next)if(L(o,l,a.next)){const E=a.anchor;a.anchor=a.next,a.next=null,a.anchorTracked=!1,ae(E),a.slew=a.shown?{from:a.shown,frame:0}:null}else++a.nextFrames>Ue&&(console.warn(`[ar] qr ${a.target.text}: the new anchor was not tracked in ${Ue} frames: given up`),ae(a.next),a.next=null);if(!a.anchor)continue;const u=L(o,l,a.anchor);if(!u){a.anchorTracked&&(a.anchorTracked=!1,M(a,!1,"lost (its anchor is not tracked)"));continue}a.anchorTracked=!0,a.anchorPose=u;let y=u;a.slew&&(a.slew.frame++,y=Pt(a.slew.from,u,Math.min(1,a.slew.frame/He)),a.slew.frame>=He&&(a.slew=null)),a.shown=y,e.setMatrix(a.target.entity,Me(y)),M(a,!0,"regained (its anchor is tracked again)")}},K=(o,l,a,u,y)=>{const{picture:E}=a,P=l[1],q=l[2],U=l[3],B=l.subarray(4,20),ee=performance.now();if(o.misses=0,U<Ne){ee-o.keptLoggedAt>=Ae&&(o.keptLoggedAt=ee,console.log(`[ar] qr ${o.target.text}: ignored (${U} points < ${Ne}; side ${P.toFixed(0)} px of ${E.width}, rms ${q.toFixed(2)} px)`));return}q<=ve.max_rms_px&&k(o,E.viewMatrix,{x:B[12],y:B[13],z:B[14]},ee);const O=o.depth.scale,D=Array.from(B);D[12]=B[12]*O,D[13]=B[13]*O,D[14]=B[14]*O;const C=Ft(E.viewMatrix,D),T=It(C);o.corners=Array.from(l.subarray(20,28)),o.cornersIn=[E.width,E.height],o.side=P;const W=P*ke/E.width;ee-o.loggedAt>=Yt&&(o.loggedAt=ee,console.log(`[ar] qr ${o.target.text}: side ${P.toFixed(0)} px of ${E.width}, rms ${q.toFixed(2)} px, inliers ${U}, t=(${B[12].toFixed(3)} ${B[13].toFixed(3)} ${B[14].toFixed(3)}) m, the picture after ${a.capturedAt-a.askedAt} frames, the result after ${y}, worker ${u.toFixed(1)} ms`));const re=I(o);if(re==="waiting")return;if(re==="seeking"){o.anchor&&(ae(o.anchor),o.anchor=null,o.anchorPose=null),o.slew=null,o.shown=T,o.bestSide=W,e.setMatrix(o.target.entity,C),M(o,!0,`found at (${T.position.x.toFixed(3)} ${T.position.y.toFixed(3)} ${T.position.z.toFixed(3)}) m`),e.anchors()&&F(o,T);return}const le=o.anchorPose,me=Ce(T.position,le.position),ie=De(T.rotation,le.rotation),Q=en({metres:me,degrees:ie,side:W,rms:q,inliers:U},o.bestSide),ne=`Δ ${(me*1e3).toFixed(1)} mm, ${ie.toFixed(2)}°`;Q==="near"?o.bestSide=Math.max(o.bestSide,W):Q==="kept"?ee-o.keptLoggedAt>=Ae&&(o.keptLoggedAt=ee,console.log(`[ar] qr ${o.target.text}: kept (side ${W.toFixed(0)} < ${$e} × ${o.bestSide.toFixed(0)} px), ${ne} (rms ${q.toFixed(2)} px)`)):(Q==="replace"||Q==="move")&&(console.log(`[ar] qr ${o.target.text}: anchor replaced${Q==="move"?" (a large Δ)":""}, ${ne} (side ${W.toFixed(0)} px, best ${o.bestSide.toFixed(0)}, rms ${q.toFixed(2)} px)`),o.bestSide=W,F(o,T))},te=o=>{if(N(),o.type==="ready"){i=!0,console.log(`[ar] qr: qr-anchor ${o.version} loaded in its worker in ${o.ms.toFixed(0)} ms`);return}if(o.type==="failed"){H(o.message);return}const l=c;if(!l||o.seq!==l.seq)return;c=null,d=!1;const a=o.observations,u=Math.floor(a.length/pe),y=p-l.askedAt;if(x.since&&l.phase===x.phase&&(x.captures++,x.workerMs+=o.processMs,x.captureFrames+=l.capturedAt-l.askedAt,x.resultFrames+=y,u>0&&x.found++),!l.stale){if(l.phase==="holding"){const E=new Set;for(let P=0;P<u;P++)E.add(a[P*pe]);t.forEach((P,q)=>{!E.has(q)&&I(P)==="holding"&&P.misses++})}for(let E=0;E<u;E++){const P=a.subarray(E*pe,(E+1)*pe),q=t[P[0]];q&&K(q,P,l,o.processMs,y)}}},Z=(o,l,a)=>{if(!r||!i){d=!1;return}const{width:u,height:y}=o,E=o.projection;if(!b.has(u)){b.add(u);const O=E[0]*u/2,D=E[5]*y/2,C=(1-E[8])*u/2-.5,T=(1+E[9])*y/2-.5;console.log(`[ar] qr: the capture is ${u}×${y} gray (seeking ${We} px / holding ${ke} px, worker), fx ${O.toFixed(1)} fy ${D.toFixed(1)} cx ${C.toFixed(1)} cy ${T.toFixed(1)} px`)}const P=[],q=ze(o.viewMatrix);t.forEach((O,D)=>{if(I(O)!=="holding")return;let C=1/0,T=1/0,W=-1/0,re=-1/0,le=0;const me=O.anchorPose;if(me&&q){const Q=Me(me),ne=O.target.size/2;for(const[se,de]of[[-ne,-ne],[ne,-ne],[ne,ne],[-ne,ne]]){const ye=Q[0]*se+Q[8]*de+Q[12],Je=Q[1]*se+Q[9]*de+Q[13],et=Q[2]*se+Q[10]*de+Q[14],we=_t({x:ye,y:Je,z:et},E,q,u,y);if(!we){C=1/0;break}C=Math.min(C,we[0]),W=Math.max(W,we[0]),T=Math.min(T,we[1]),re=Math.max(re,we[1])}le=Math.max(W-C,re-T)}if(!(C<1/0)&&O.corners){const Q=u/O.cornersIn[0],ne=y/O.cornersIn[1];C=1/0,T=1/0,W=-1/0,re=-1/0;for(let se=0;se<4;se++){const de=O.corners[2*se]*Q,ye=O.corners[2*se+1]*ne;C=Math.min(C,de),W=Math.max(W,de),T=Math.min(T,ye),re=Math.max(re,ye)}le=O.side*Math.max(Q,ne)}if(!(C<1/0))return;const ie=Qt*le;P.push([D,Math.floor(C-ie),Math.floor(T-ie),Math.ceil(W+ie),Math.ceil(re+ie)])});const U=o.data,B=U.byteOffset===0&&U.byteLength===U.buffer.byteLength?U.buffer:U.slice().buffer,ee=++m;c={seq:ee,phase:l,picture:o,askedAt:a,capturedAt:p,stale:!1},j({type:"frame",seq:ee,phase:l,gray:B,width:u,height:y,p0:E[0],p5:E[5],p8:E[8],p9:E[9],hints:P},[B])},w=o=>{const l=g;return g=o,!!l&&Ce(o.position,l.position)<Ut&&De(o.rotation,l.rotation)<Gt},A=(o,l,a)=>(l?o/l:0).toFixed(a);return{setCodes(o){const l=o.map(u=>t.find(y=>y.target.entity===u.entity&&y.target.text===u.text&&y.target.size===u.size)??X(u));for(const u of t)l.includes(u)||J(u,!0);const a=l.length!==t.length||l.some((u,y)=>u!==t[y]);t=l,a&&(c&&(c.stale=!0),j({type:"targets",targets:_()})),Y()},frame(o,l,a){if(p++,t.length===0||!l)return;G(o,l);const u=a==null?void 0:a.views[0];if(!a||!u||a.emulatedPosition){g=null;return}const y=w(Ge(a.transform));v++;const E=t.map(I),P=t.some((O,D)=>E[D]==="seeking"||E[D]==="holding"&&O.misses>=be);if(P||E.includes("holding")){const O=performance.now(),D=P?"seeking":"holding";if(!x.since||x.phase!==D)x.phase=D,R(O);else if(O-x.since>=Zt){const C=x.captures,T=t.map(W=>t.length>1?`${W.target.text} ${W.depth.scale.toFixed(3)}`:W.depth.scale.toFixed(3)).join(", ");console.log(`[ar] qr: ${D} — ${C} captures in ${((O-x.since)/1e3).toFixed(0)} s, ${x.found} with a code, the picture after ${A(x.captureFrames,C,1)} frames, the result after ${A(x.resultFrames,C,1)}, worker ${A(x.workerMs,C,1)} ms mean, depth scale ${T}${t.some(W=>W.misses>=be)?`, sought again after ${be} misses`:""}`),R(O)}}else x.since=0;if(d||!i)return;let q;if(P)q=v>=Wt;else{const O=t.some((C,T)=>E[T]==="holding"&&C.depth.estimate===null)?Ht:Nt,D=v>=O&&y?ze(u.transform.matrix):null;q=!!D&&t.some((C,T)=>E[T]==="holding"&&C.anchorPose&&Ot(C.anchorPose.position,u.projectionMatrix,D))}if(!q)return;v=0,d=!0;const U=n,B=p,ee=P?"seeking":"holding";e.capture(P?We:ke).then(O=>{U===n&&(O?Z(O,ee,B):d=!1)},O=>{U===n&&(d=!1,console.warn(`[ar] qr: the capture failed (${Be(O)})`))})},clear(o=!1){n++,Fe=o;try{for(const l of t)J(l,!1)}finally{Fe=!1}t=[],V(),h=!1,v=0,g=null,b.clear(),R(0)}}},Ve=["hit-test","anchors","dom-overlay","camera-access","light-estimation"],rn=4,on=100,Xe=new Set,oe=(e,t)=>{Xe.has(e)||(Xe.add(e),console.warn(`[ar] ${t}`))},sn=async e=>{const t=typeof navigator>"u"?void 0:navigator.xr;if(!t||typeof e.presentXR!="function")return!1;try{return await t.isSessionSupported("immersive-ar")}catch{return!1}},Ke=(e,t,n,r)=>{const s=e[3]*t+e[7]*n+e[11]*r+e[15];return[(e[0]*t+e[4]*n+e[8]*r+e[12])/s,(e[1]*t+e[5]*n+e[9]*r+e[13])/s,(e[2]*t+e[6]*n+e[10]*r+e[14])/s]},an=(e,t,n,r)=>{const s=t.scene;let i=0,f=!1,h=!1,c=null,m=null,d=null,p=null,v=null,g=new Set,b="",x=!1,R=null,$=[0,0,0],M=null,_=-1/0,j=-1,N=!0,V=[],H=[];const Y=new Map,X=new Map,J=nn({setMatrix:(w,A)=>e.setMatrix(w,A),image:w=>n.image(w),capture:w=>p?p.captureCamera({gray:!0,maxWidth:w}):Promise.resolve(null),anchor:w=>{H.push(w)},anchors:()=>g.has("anchors"),workerUrl:async()=>{var w;return(w=await(r==null?void 0:r()))==null?void 0:w.qrAnchorWorker}}),k=w=>w.preventDefault(),I=w=>{let A="normal",o;(!w||w.emulatedPosition)&&(A="limited",o=x?"relocalizing":"initializing"),w&&!w.emulatedPosition&&(x=!0);const l=`${A}:${o??""}`;l!==b&&(b=l,n.tracking(o?{state:A,reason:o}:{state:A}))},F=(w,A)=>{const o=[];for(const l of w.getHitTestResults(A)){const a=m?l.getPose(m):void 0;if(!a)continue;const u=a.transform.position,y=a.transform.orientation;o.push({type:"estimated",pose:{position:{x:u.x,y:u.y,z:u.z},rotation:{x:y.x,y:y.y,z:y.z,w:y.w}},distance:Math.hypot(u.x-$[0],u.y-$[1],u.z-$[2])})}return o.sort((l,a)=>l.distance-a.distance)},L=(w,A)=>{var y;if(!A.wanted()){A.settle(null,!1);return}const o=i,{position:l,rotation:a}=A.pose;let u;try{u=m?(y=w.createAnchor)==null?void 0:y.call(w,new XRRigidTransform({x:l.x,y:l.y,z:l.z,w:1},{x:a.x,y:a.y,z:a.z,w:a.w}),m):void 0}catch(E){u=Promise.reject(E)}if(!u){A.settle(null,!1);return}u.then(E=>{if(o!==i||!A.wanted()){E.delete(),A.settle(null,o!==i);return}A.settle(E,!1)},E=>{oe("anchor",`the session made no anchor (${E instanceof Error?E.message:E}): an anchored node stays where it was put`),A.settle(null,o!==i)})},G=(w,A)=>{var u;if(!M||w-_<on)return;const o=(u=A.getLightEstimate)==null?void 0:u.call(A,M);if(!o)return;_=w;const l=o.primaryLightIntensity,a=Math.max(l.x,l.y,l.z)*1e3;Math.abs(a-j)<1||(j=a,n.light({intensity:a,temperature:6500}))},K=(w,A,o)=>{if(A.session!==c)return;I(o);const l=o==null?void 0:o.views[0];if(o&&l){R=ze(l.projectionMatrix);const a=o.transform.position;$=[a.x,a.y,a.z]}for(const[a,u]of X){const E=(A.trackedAnchors?A.trackedAnchors.has(u.anchor):!0)&&m?A.getPose(u.anchor.anchorSpace,m):void 0;E&&e.setMatrix(a,E.transform.matrix);const P=!!E;P&&(u.seen=!0),P!==u.tracked&&u.seen&&(u.tracked=P,n.anchor({entity:a,tracked:P}))}if(J.frame(A,m,o),H.length){const a=H;H=[];for(const u of a)L(A,u)}V.length&&(V=V.filter(a=>{const u=F(A,a.source);return u.length===0&&++a.frames<rn?!0:(a.source.cancel(),a.resolve(u),!1)})),G(w,A),v==null||v()},te=(w=!1)=>{var l;i++,f=!1,h=!1,v==null||v(new z("failed")),p==null||p.end(),p=null;const A=c;c=null,A&&(A.removeEventListener("end",Z),w||A.end().catch(()=>{}),s!==void 0&&e.clearProjection(s)),(l=document.body)==null||l.removeEventListener("beforexrselect",k);for(const a of V){if(!w)try{a.source.cancel()}catch{}a.reject(new z("notRunning"))}V=[];const o=H;H=[];for(const a of o)a.settle(null,!0);if(Y.clear(),!w)for(const a of X.values())try{a.anchor.delete()}catch{}X.clear(),J.clear(w),m=null,d=null,g=new Set,b="",x=!1,R=null,$=[0,0,0],M=null,_=-1/0,j=-1},Z=()=>{const w=f;te(!0),w&&n.ended({reason:"user"})};return{async start(w){var u;if(f||h)throw new z("busy");if(s===void 0||t.camera===void 0)throw console.warn("[ar] start: the session has no scene or camera to drive (ARScene opens it with both)"),new z("failed");const A=navigator.xr,o=e.presentXR;if(!A||!o)throw new z("unavailable");((w==null?void 0:w.mode)==="images"||(u=w==null?void 0:w.images)!=null&&u.length)&&console.warn("[ar] the web finds no images in the camera's picture: their anchors are never tracked"),(w==null?void 0:w.worldAlignment)==="gravityAndHeading"&&oe("heading","WebXR has no compass: the world is aligned with gravity alone, −Z where the session began"),h=!0;const l=++i;J.setCodes((w==null?void 0:w.codes)??[]);let a;try{a=await A.requestSession("immersive-ar",{requiredFeatures:["local"],optionalFeatures:Ve,domOverlay:{root:document.body}})}catch(y){l===i&&(h=!1);const E=y instanceof Error?y.name:"";throw console.warn(`[ar] no AR session: ${y instanceof Error?y.message:y}`),new z(E==="NotAllowedError"||E==="SecurityError"?"denied":E==="NotSupportedError"?"unavailable":"failed")}if(l!==i)throw a.end().catch(()=>{}),new z("failed");c=a,a.addEventListener("end",Z),document.body.addEventListener("beforexrselect",k),g=new Set(a.enabledFeatures??["local",...Ve]);try{const y=await a.requestReferenceSpace("local");if(m=y,d=await a.requestReferenceSpace("viewer"),w!=null&&w.lightEstimation){const q=a.requestLightProbe;g.has("light-estimation")&&q?M=await q.call(a).catch(U=>(oe("light",`no light estimate: ${U instanceof Error?U.message:U}`),null)):oe("light","the AR session has no light estimate: no light event comes")}if(l!==i)throw new z("failed");const E=new Promise((q,U)=>{v=B=>{v=null,B?U(B):q()}}),P=await o.call(e,s,a,y,K);if(l!==i)throw P.end(),new z("failed");p=P,await E}catch(y){throw y instanceof z||console.warn(`[ar] the AR session did not start: ${y instanceof Error?y.message:y}`),l===i&&te(),y instanceof z?y:new z("failed")}if(l!==i)throw new z("failed");h=!1,f=!0},stop(){te()},setImages(w){if(!f)throw new z("notRunning");w.length&&console.warn("[ar] the web finds no images in the camera's picture: their anchors are never tracked")},setCodes(w){if(!f)throw new z("notRunning");J.setCodes(w)},async hitTest(w,A,o){const l=c;if(!f||!l||!d||s===void 0)throw new z("notRunning");if(!g.has("hit-test")||!l.requestHitTestSource)return oe("hit-test","the AR session has no hit test: hitTest finds nothing"),[];const a=e.viewport(s),u=R;if(!u||!a.width||!a.height)return[];const y=w/a.width*2-1,E=1-A/a.height*2,P=Ke(u,y,E,-1),q=Ke(u,y,E,1),U={x:q[0]-P[0],y:q[1]-P[1],z:q[2]-P[2],w:0},B=new XRRay({x:0,y:0,z:0,w:1},U),ee=i,O=async T=>l.requestHitTestSource({space:d,offsetRay:B,entityTypes:T});let D;try{D=await O(o==="planes"||!N?["plane"]:["plane","point"])}catch(T){if(o==="planes"||!N||ee!==i){if(oe("hit-source",`the session made no hit test: ${T instanceof Error?T.message:T}`),ee!==i)throw new z("notRunning");return[]}N=!1;try{D=await O(["plane"])}catch(W){oe("hit-source",`the session made no hit test: ${W instanceof Error?W.message:W}`)}}if(ee!==i)throw D==null||D.cancel(),new z("notRunning");if(!D)return[];const C=D;return new Promise((T,W)=>{V.push({source:C,frames:0,resolve:T,reject:W})})},anchor(w,A){if(!f||!c)throw new z("notRunning");if(e.setMatrix(w,Me(A)),!g.has("anchors")){oe("anchors","the AR session has no anchors: an anchored node stays where it was put (the world map does not refine it)");return}const o=X.get(w);if(o){X.delete(w);try{o.anchor.delete()}catch{}}return new Promise((l,a)=>{const u={pose:A,wanted:()=>Y.get(w)===u,settle:(y,E)=>{Y.get(w)===u&&Y.delete(w),y&&X.set(w,{anchor:y,tracked:!0,seen:!1}),E?a(new z("notRunning")):l()}};Y.set(w,u),H.push(u)})},release(w){Y.delete(w);const A=X.get(w);if(A){X.delete(w);try{A.anchor.delete()}catch{}}},async captureFrame(){const w=p;if(!f||!w)throw new z("notRunning");const A=i,o=await w.captureCamera();if(A!==i)throw new z("notRunning");if(!o)throw oe("capture","captureFrame: the AR session gives no camera picture (WebXR's camera-access: Chrome on Android, the camera allowed)"),new z("failed");const l=document.createElement("canvas");l.width=o.width,l.height=o.height;const a=l.getContext("2d");if(!a)throw new z("failed");a.putImageData(new ImageData(new Uint8ClampedArray(o.rgba.buffer,o.rgba.byteOffset,o.rgba.byteLength),o.width,o.height),0,0);const u=await new Promise(y=>l.toBlob(y,"image/jpeg",.9));if(!u)throw new z("failed");return{data:await u.arrayBuffer(),name:"frame.jpg"}},close(){te()}}},cn=(e,t)=>{var r;if(typeof navigator>"u"||!((r=navigator.mediaDevices)!=null&&r.getUserMedia))return;const n=e.gl;yt(e,(s,i)=>{let f=null,h=null,c=null,m=!1,d=0;const p=()=>{if(!c)throw new z("notRunning");return c},v=()=>{d++;const g=c;return c=null,g};return{async start(g){if(m)throw new z("busy");if(c)return c.start(g);m=!0;const b=d;let x;try{x=await sn(n)?h??(h=an(n,s,i,t)):f??(f=zt(n,s,i))}finally{m=!1}if(b!==d)throw new z("failed");c=x;try{await x.start(g)}catch(R){throw c===x&&(c=null),R}},stop(){var g;return(g=v())==null?void 0:g.stop()},setImages(g){return p().setImages(g)},setCodes(g){return p().setCodes(g)},hitTest(g,b,x){return p().hitTest(g,b,x)},anchor(g,b){return p().anchor(g,b)},release(g){return c==null?void 0:c.release(g)},captureFrame(){return p().captureFrame()},close(){var g,b;(b=(g=v())==null?void 0:g.close)==null||b.call(g)}}})},dn=e=>cn(e,()=>st(()=>import("./_virtual_lecodes-half-resources-CNYmVR88.js"),[],import.meta.url).then(t=>t.resources));export{dn as register};
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "plugins": {
3
- "ar": "plugins/ar/index-CHvaraG4.js",
3
+ "ar": "plugins/ar/index-C2uuuLhz.js",
4
4
  "audio-recorder": "plugins/audio-recorder/index-Bt-Caym7.js",
5
5
  "browser": "plugins/browser/index-BpFb50ko.js",
6
6
  "camera": "plugins/camera/index-5XOM8eHl.js",
@@ -13,9 +13,10 @@ import {
13
13
  } from "./templates/ios"
14
14
  import {
15
15
  FIRST_PARTY_PLUGINS, SDK_VARIANTS, cmpVersion, ensureShellIgnored,
16
- fetchLatestSdkVersion, isLocalPluginSpec, latestPluginTag, projectRoot, readAppConfigAt, readJson, resolvePlugin, resolveSdkVersion, sdkSatisfies,
16
+ fetchLatestSdkVersion, latestPluginTag, localPlugin, localPluginKey, pluginSpecsFor, projectRoot, readAppConfigAt, readJson, resolvePlugin, resolveSdkVersion, sdkSatisfies,
17
17
  resolveVariant, runTool, slugify, writeJson, VERSION_RE, type AppConfig, type LockEntry, resolveUpdateUrl, urlSchemeOf,
18
18
  } from "./shared"
19
+ import { isLocalPluginSpec, type PluginEntry } from "lecodes-sdk/compile"
19
20
  import {
20
21
  ANDROID_SDK_MIN_VERSION, ANDROID_SDK_REPO, androidBuild, doctorAndroidChecks, initAndroid,
21
22
  keystore, openAndroid, shellSdkVersionAndroid, syncAndroid, type SyncArtifacts,
@@ -443,33 +444,48 @@ const sync = defineCommand({
443
444
 
444
445
  /* -------------------------------------- plugin -------------------------------------- */
445
446
 
446
- const pluginConfig = (): { root: string, configPath: string, config: AppConfig & { plugins: Record<string, string> } } => {
447
+ const pluginConfig = (): { root: string, configPath: string, config: AppConfig & { plugins: Record<string, PluginEntry> } } => {
447
448
  const root = projectRoot()
448
449
  const configPath = join(root, "app.json")
449
450
  const config = readAppConfigAt(root)
450
451
  if (!config) throw new CliError("No app.json here — run `lecodes app init <ios|android>` first.")
451
452
  config.plugins ??= {}
452
- return { root, configPath, config: config as AppConfig & { plugins: Record<string, string> } }
453
+ return { root, configPath, config: config as AppConfig & { plugins: Record<string, PluginEntry> } }
453
454
  }
454
455
 
456
+ /** An entry as app.json says it: one spec, or `ios: …, web: …`. */
457
+ const entryText = (entry: PluginEntry): string =>
458
+ typeof entry === "string" ? entry : Object.entries(entry).map(([p, s]) => `${p}: ${s}`).join(", ") || "*"
459
+
455
460
  const pluginList = (): void => {
456
461
  const { root, config } = pluginConfig()
457
462
  const entries = Object.entries(config.plugins)
458
463
  if (entries.length === 0) { log("No plugins installed."); return }
459
464
  const lock = readJson<{ plugins?: Record<string, LockEntry> }>(join(root, "plugins.lock.json"))?.plugins ?? {}
460
- for (const [id, spec] of entries) {
465
+ for (const [id, entry] of entries) {
461
466
  const pin = lock[id] ? c.dim(` ${lock[id].repo} ${lock[id].tag} (${lock[id].commit.slice(0, 7)})`) : c.dim(" (not synced)")
462
- log(`${c.bold(id)} ${spec}${pin}`)
467
+ log(`${c.bold(id)} ${entryText(entry)}${pin}`)
463
468
  }
464
469
  }
465
470
 
466
471
  const pluginAdd = (id: string | undefined): void => {
467
472
  if (!id) throw new UsageError("Which plugin? `lecodes app plugin add <id | github:owner/repo[@tag] | ./plugins/<id>>`")
468
- const { configPath, config } = pluginConfig()
473
+ const { root, configPath, config } = pluginConfig()
469
474
  if (isLocalPluginSpec(id)) {
470
475
  const bare = basename(id.replace(/\/+$/, ""))
471
- config.plugins[bare] = id
472
- log(`${c.green("added")} ${bare} → ${id} ${c.dim("(a plugin of this project: its folder, as it is)")}`)
476
+ // A half folder is named for the platforms it has a half of, under the plugin it implements:
477
+ // the first-party halves stay on the others (the entry's spec for them is kept).
478
+ const { manifest } = localPlugin(root, localPluginKey(root, id), id)
479
+ if (manifest.implements) {
480
+ const was = config.plugins[manifest.implements] ?? "*"
481
+ const kept: Record<string, string> = typeof was === "string" ? (was === "*" ? {} : { ios: was, android: was, web: was }) : { ...was }
482
+ for (const p of ["ios", "android", "web"] as const) if (manifest[p]) kept[p] = id
483
+ config.plugins[manifest.implements] = kept
484
+ log(`${c.green("added")} ${manifest.implements} → ${entryText(kept)} ${c.dim(`(a half of this project, over the first-party plugin)`)}`)
485
+ } else {
486
+ config.plugins[bare] = id
487
+ log(`${c.green("added")} ${bare} → ${id} ${c.dim("(a plugin of this project: its folder, as it is)")}`)
488
+ }
473
489
  } else if (id.startsWith("github:")) {
474
490
  const bare = id.slice("github:".length).split("@")[0].split("/").pop()!
475
491
  config.plugins[bare] = id
@@ -591,7 +607,8 @@ export const runAppUpdate = async (opts: AppUpdateOptions = {}): Promise<void> =
591
607
  // ---- plugins ----
592
608
  const lockPath = join(root, "plugins.lock.json")
593
609
  const lock = readJson<{ plugins?: Record<string, LockEntry> }>(lockPath)?.plugins ?? {}
594
- const specs = Object.entries(config!.plugins ?? {})
610
+ // (one lock for the shells: the spec of the shell being updated — the first, with both)
611
+ const specs = pluginSpecsFor(config, doIos ? "ios" : "android")
595
612
  if (specs.length > 0 && !opts.noPlugins) {
596
613
  const kept: Record<string, LockEntry> = {}
597
614
  for (const [id, spec] of specs) {
@@ -1,13 +1,13 @@
1
1
  import { execFileSync } from "node:child_process"
2
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
3
3
  import { homedir } from "node:os"
4
- import { dirname, join, resolve } from "node:path"
4
+ import { basename, dirname, join, resolve } from "node:path"
5
5
  import { findProjectRoot, readManifest, type Manifest } from "../../project/manifest"
6
6
  import { loadConfig, requireApiUrl } from "../../platform/config"
7
7
  import { DEFAULT_IGNORE_FILE } from "../../project/ignore"
8
8
  import { CliError, c, log, note, warn } from "../../cli"
9
9
  import { pascal, readManifest as readPluginManifest, type IosPackage, type PluginManifest as PluginManifest2 } from "../../plugin/manifest"
10
- import { pluginSpecsOf } from "lecodes-sdk/compile"
10
+ import { PLUGIN_PLATFORMS, isLocalPluginSpec, pluginEntriesOf, pluginSpecOf, type PluginEntry } from "lecodes-sdk/compile"
11
11
 
12
12
  /** The 2.x manifest (plugin/manifest.ts), plus the desktop branch the desktop flow reads: a
13
13
  * PREBUILT shared library per OS/arch behind the C ABI in hosts/desktop/plugin-sdk — another
@@ -23,7 +23,12 @@ export type PluginManifest = PluginManifest2 & { desktop?: { library?: Record<st
23
23
  /** First-party plugin registry: id → GitHub repo holding plugins/<id>/. Third-party plugins
24
24
  * use a `github:owner/repo[@tag]` spec in app.json instead (manifest at the repo root); a plugin
25
25
  * of the project itself a PATH (`./plugins/<id>` — the folder with its lecodes-plugin.json,
26
- * generated by `lecodes plugin gen`; nothing to publish). */
26
+ * generated by `lecodes plugin gen`; nothing to publish).
27
+ * What a SHELL can vendor: the plugins of the repo's released v* tags (a sync checks out the
28
+ * newest). The newer first-party plugins (ar, speech, audio-recorder, webrtc) are in the viewers
29
+ * and in the SDK, and in no tag yet — an id joins this list with the tag that carries it; listing
30
+ * one before would make `app sync` clone a tag without its folder. compile, link and the web
31
+ * builds never read this list (the header lists app.json's keys; the web halves are Web Lite's). */
27
32
  export const FIRST_PARTY_PLUGINS: Record<string, string> = {
28
33
  "camera": "letary/lecodes-plugins",
29
34
  "qr-scanner": "letary/lecodes-plugins",
@@ -44,7 +49,10 @@ export interface AppConfig {
44
49
  /** Icon backdrop ("#RGB"/"#RRGGBB", default white): the Android adaptive-icon background
45
50
  * layer, and what a transparent master is flattened onto for the opacity-required iOS icon. */
46
51
  iconBackground?: string
47
- plugins?: Record<string, string>
52
+ /** id → a spec for every platform (`"*"`, `"^2.0.0"`, `"github:owner/repo@v1"`, `"./plugins/<id>"`)
53
+ * or a spec per platform (`{ "web": "./plugins/ar-8thwall" }`; a platform it does not name is
54
+ * `"*"`). Read in the list form too (readAppConfigAt); a platform's spec is pluginSpecOf's. */
55
+ plugins?: Record<string, PluginEntry>
48
56
  /** The custom URL scheme the shells open on (`myshop` → `myshop://…`): iOS CFBundleURLTypes,
49
57
  * Android a BROWSABLE intent-filter on the activity. A link reaches the app as `app.launchUrl`
50
58
  * / the "url" event — what `Browser.open(url, { returnUrl })` resolves on after an OAuth redirect.
@@ -226,12 +234,33 @@ export const urlSchemeOf = (config: Pick<AppConfig, "urlScheme"> | null | undefi
226
234
  * this is consulted. */
227
235
  export const readAppConfigAt = (root: string): AppConfig | null => {
228
236
  const config = readJson<AppConfig & { plugins?: unknown }>(join(root, "app.json"))
229
- // `"plugins"` is read in either shape (`["camera"]` or `{ "camera": "*" }`, the SDK's rule):
230
- // every command below works on id → spec, and a list read as "no plugins" vendored nothing.
231
- if (config && "plugins" in config) config.plugins = pluginSpecsOf(config.plugins)
237
+ // `"plugins"` is read in any shape (`["camera"]`, `{ "camera": "*" }`, `{ "ar": { "web": … } }`,
238
+ // the SDK's rule): every command below works on id → entry, and a list read as "no plugins"
239
+ // vendored nothing. An entry of no shape is refused here, by its key — the SDK reads it as "*".
240
+ if (config && "plugins" in config) {
241
+ checkPluginEntries(config.plugins)
242
+ config.plugins = pluginEntriesOf(config.plugins)
243
+ }
232
244
  return config as AppConfig | null
233
245
  }
234
246
 
247
+ const SPEC_WORDS = `a spec ("*", "^2.0.0", "github:owner/repo[@tag]", "./plugins/<id>") or a spec per platform ({ ${PLUGIN_PLATFORMS.map((p) => `"${p}"`).join(", ")} } → spec; a platform not named is "*")`
248
+
249
+ const checkPluginEntries = (plugins: unknown): void => {
250
+ if (plugins === undefined || Array.isArray(plugins)) return
251
+ if (!plugins || typeof plugins !== "object") throw new CliError(`app.json "plugins": a list of ids, or id → ${SPEC_WORDS}.`)
252
+ for (const [id, value] of Object.entries(plugins as Record<string, unknown>)) {
253
+ if (typeof value === "string" && value.trim() !== "") continue
254
+ const ok = !!value && typeof value === "object" && !Array.isArray(value) &&
255
+ Object.entries(value as Record<string, unknown>).every(([p, s]) => (PLUGIN_PLATFORMS as readonly string[]).includes(p) && typeof s === "string" && s.trim() !== "")
256
+ if (!ok) throw new CliError(`app.json "plugins"."${id}": ${SPEC_WORDS} — got ${JSON.stringify(value)}.`)
257
+ }
258
+ }
259
+
260
+ /** app.json's plugins as one platform takes them: id → that platform's spec. */
261
+ export const pluginSpecsFor = (config: Pick<AppConfig, "plugins"> | null | undefined, platform: PluginPlatform): [string, string][] =>
262
+ Object.entries(config?.plugins ?? {}).map(([id, entry]) => [id, pluginSpecOf(entry, platform)])
263
+
235
264
  /** The desktop host's own built-in fallback, mirrored where the CLI must fill a missing side. */
236
265
  export const DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 }
237
266
 
@@ -278,10 +307,6 @@ export type { IosPackage }
278
307
 
279
308
  export interface LockEntry { repo: string, tag: string, commit: string, version: string }
280
309
 
281
- /** A plugin spec that names a folder of the project (`./plugins/x`, `../x`, `/abs`, `file:x`). */
282
- export const isLocalPluginSpec = (spec: string): boolean =>
283
- spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || spec.startsWith("file:")
284
-
285
310
  /** Resolve app.json "update" to the URL a shell's native updater checks (LeCodesUpdater on both
286
311
  * platforms): absent or `true` = the project's live bundle on the platform —
287
312
  * `/code/<uuid>.js?sdk=<version>`, the launcher's own URL, so a pushed project updates its shells
@@ -548,18 +573,43 @@ export const ensureRepo = (repo: string, wantTag: string | null, lock: LockEntry
548
573
 
549
574
  /** Resolve one app.json plugin entry to vendorable sources + manifest + lock pin. The platform
550
575
  * being synced must be supported (its manifest branch non-null) — a hard error otherwise. */
551
- export type PluginPlatform = "ios" | "android" | "desktop"
552
- const PLUGIN_PLATFORM_LABEL: Record<PluginPlatform, string> = { ios: "iOS", android: "Android", desktop: "desktop" }
576
+ export type PluginPlatform = "ios" | "android" | "desktop" | "web"
577
+ const PLUGIN_PLATFORM_LABEL: Record<PluginPlatform, string> = { ios: "iOS", android: "Android", desktop: "desktop", web: "the web" }
578
+
579
+ /** A plugin of the project itself (a local spec): its folder and its manifest. The key of app.json
580
+ * is the plugin's id: the folder's own (`"acme-dial": "./plugins/acme-dial"`), or — for a HALF
581
+ * FOLDER — the first-party plugin it implements (`"ar": { "web": "./plugins/ar-8thwall" }`). */
582
+ export const localPlugin = (root: string, id: string, spec: string): { dir: string, manifest: PluginManifest } => {
583
+ const dir = resolve(root, spec.startsWith("file:") ? spec.slice("file:".length) : spec)
584
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) throw new CliError(`Plugin "${id}": ${spec} is not a folder (${dir})`)
585
+ const folder = basename(dir)
586
+ const manifest: PluginManifest = readPluginManifest(dir, folder)
587
+ if (manifest.implements ? manifest.implements !== id : folder !== id) {
588
+ throw new CliError(manifest.implements
589
+ ? `Plugin "${id}": ${spec} implements "${manifest.implements}" — app.json names it under that plugin: "${manifest.implements}": { "<platform>": "${spec}" }`
590
+ : `Plugin "${id}": ${spec} is the plugin "${folder}" — app.json names a plugin of the project by its folder's name`)
591
+ }
592
+ fillManifestDefaults(manifest, folder)
593
+ return { dir, manifest }
594
+ }
595
+
596
+ /** The key app.json names a local plugin under: its folder's name, or the first-party plugin a half
597
+ * folder implements. */
598
+ export const localPluginKey = (root: string, spec: string): string => {
599
+ const dir = resolve(root, spec.startsWith("file:") ? spec.slice("file:".length) : spec)
600
+ if (!existsSync(join(dir, "lecodes-plugin.json"))) return basename(dir)
601
+ return readPluginManifest(dir, basename(dir)).implements ?? basename(dir)
602
+ }
553
603
 
554
- export function resolvePlugin(id: string, spec: string, lock: LockEntry | undefined, platform: PluginPlatform): ResolvedPlugin
555
- export function resolvePlugin(id: string, spec: string, lock: LockEntry | undefined, platform: PluginPlatform, opts: { optional: true }): ResolvedPlugin | null
556
- export function resolvePlugin(id: string, spec: string, lock: LockEntry | undefined, platform: PluginPlatform, opts?: { optional?: boolean }): ResolvedPlugin | null {
604
+ /** `entry` is app.json's for the id, in either form: the platform's own spec is resolved (a
605
+ * per-platform entry that does not name the platform is the first-party plugin, `"*"`). */
606
+ export function resolvePlugin(id: string, entry: PluginEntry, lock: LockEntry | undefined, platform: PluginPlatform): ResolvedPlugin
607
+ export function resolvePlugin(id: string, entry: PluginEntry, lock: LockEntry | undefined, platform: PluginPlatform, opts: { optional: true }): ResolvedPlugin | null
608
+ export function resolvePlugin(id: string, entry: PluginEntry, lock: LockEntry | undefined, platform: PluginPlatform, opts?: { optional?: boolean }): ResolvedPlugin | null {
609
+ const spec = pluginSpecOf(entry, platform)
557
610
  // A plugin of the project itself: its folder, as it is — no clone, no tag, nothing to publish.
558
611
  if (isLocalPluginSpec(spec)) {
559
- const dir = resolve(projectRoot(), spec.startsWith("file:") ? spec.slice("file:".length) : spec)
560
- if (!existsSync(dir)) throw new CliError(`Plugin "${id}": ${spec} is not a folder (${dir})`)
561
- const manifest: PluginManifest = readPluginManifest(dir, id)
562
- fillManifestDefaults(manifest, id)
612
+ const { dir, manifest } = localPlugin(projectRoot(), id, spec)
563
613
  if (!manifest[platform]) {
564
614
  if (opts?.optional) return null
565
615
  throw new CliError(`Plugin "${id}" does not support ${PLUGIN_PLATFORM_LABEL[platform]} (manifest "${platform}" is null)`)
@@ -640,8 +690,8 @@ export const desktopPluginLibraries = (root: string, opts: { strict?: boolean }
640
690
  dirs.push(local)
641
691
  for (const f of readdirSync(local)) if (isDesktopPluginLibrary(f)) files.push(join(local, f))
642
692
  }
643
- const config = readJson<AppConfig>(join(root, "app.json"))
644
- const specs = Object.entries(config?.plugins ?? {})
693
+ const config = readJson<{ plugins?: unknown }>(join(root, "app.json"))
694
+ const specs = pluginSpecsFor({ plugins: pluginEntriesOf(config?.plugins) }, "desktop")
645
695
  if (specs.length === 0) return { dirs, files }
646
696
  const lock = readJson<{ plugins?: Record<string, LockEntry> }>(join(root, "plugins.lock.json"))?.plugins ?? {}
647
697
  const target = desktopPluginTarget()
@@ -14,17 +14,17 @@ const gen = defineCommand({
14
14
  name: "gen",
15
15
  summary: "Generate a plugin's files from its contract.d.ts (--check: fail on a stale one)",
16
16
  usage: "[path]",
17
- description: "The path is one plugin (a directory with a contract.d.ts) or a repo that keeps its plugins in plugins/<id>/; the current directory when omitted.\n\nWrites sdk/<id>.gen.ts — the wrapper an app calls: the wire call by call, the class with the wire's methods as its own, and the public export when the plugin has no hand-written sdk/<id>.ts. Where the plugin has an ios/ folder, writes ios/<Name>.gen.swift — the contract's types, the protocol the plugin implements and the glue that reads a call off the wire. Where it has an android/ folder, writes the same as <Name>.gen.kt into the folder of the plugin's Kotlin package (android/ is laid out as a Gradle module: the package is the folder the path under android/src/main/java narrows down to). Where it has a web/ folder, writes web/<id>.gen.ts — the same for the web half, in a file that imports nothing; a web half that imports a LeCodes package is refused, because it would run on that host only. A repo of plugins also gets its Package.swift from the manifests: the iOS halves as one SwiftPM package, a target and a product per plugin, the SDK as a package on disk (LECODES_IOS_SDK_PATH, else the repo's package.json lecodes.iosSdkPath, else ../lecodes) — what an Xcode project links the halves as (a shell vendors the sources instead). A contract the generator does not understand is an error with its line, and every problem of a file is reported at once.\n\nGenerated files are committed. Reading a contract needs TypeScript in the plugin's repo.",
17
+ description: "The path is one plugin (a directory with a contract.d.ts) or a repo that keeps its plugins in plugins/<id>/; the current directory when omitted.\n\nWrites sdk/<id>.gen.ts — the wrapper an app calls: the wire call by call, the class with the wire's methods as its own, and the public export when the plugin has no hand-written sdk/<id>.ts. Where the plugin has an ios/ folder, writes ios/<Name>.gen.swift — the contract's types, the protocol the plugin implements and the glue that reads a call off the wire. Where it has an android/ folder, writes the same as <Name>.gen.kt into the folder of the plugin's Kotlin package (android/ is laid out as a Gradle module: the package is the folder the path under android/src/main/java narrows down to). Where it has a web/ folder, writes web/<id>.gen.ts — the same for the web half, in a file that imports nothing; a web half that imports a LeCodes package is refused, because it would run on that host only. A repo of plugins also gets its Package.swift from the manifests: the iOS halves as one SwiftPM package, a target and a product per plugin, the SDK as a package on disk (LECODES_IOS_SDK_PATH, else the repo's package.json lecodes.iosSdkPath, else ../lecodes) — what an Xcode project links the halves as (a shell vendors the sources instead). A contract the generator does not understand is an error with its line, and every problem of a file is reported at once.\n\nA HALF FOLDER of a project — a manifest with `implements: \"<first-party id>\"`, no contract.d.ts and no sdk/ of its own — is generated from the contract of the plugin it implements (the one the SDK carries): `lecodes plugin gen plugins/ar-8thwall` writes web/ar.gen.ts, the half registers the service \"ar\", and app.json names the folder for that platform (`\"ar\": { \"web\": \"./plugins/ar-8thwall\" }`).\n\nGenerated files are committed. Reading a contract needs TypeScript in the plugin's repo.",
18
18
  flags: {
19
19
  check: bool("write nothing; exit 1 when a generated file is missing or stale"),
20
20
  },
21
- examples: ["lecodes plugin gen", "lecodes plugin gen plugins/camera", "lecodes plugin gen --check"],
21
+ examples: ["lecodes plugin gen", "lecodes plugin gen plugins/camera", "lecodes plugin gen plugins/ar-8thwall", "lecodes plugin gen --check"],
22
22
  run: ({ args, flags }) => {
23
23
  const report = generate(args[0] ?? ".", { check: flags.check })
24
24
 
25
25
  for (const p of report.plugins) {
26
26
  const channels = p.contract.channels.map(ch => `${ch.kind} "${ch.name}" v${ch.version}`).join(", ")
27
- info(`${c.bold(p.id)} ${c.dim(channels)}`)
27
+ info(`${c.bold(p.id)} ${c.dim(channels)}${p.implements ? c.dim(` (implements ${p.implements}: the SDK's contract)`) : ""}`)
28
28
  for (const f of p.written) log(c.green(` written ${f}`))
29
29
  for (const f of p.stale) log(c.red(` stale ${f}`))
30
30
  for (const f of p.fresh) log(c.dim(` fresh ${f}`))
@@ -3,7 +3,7 @@ import { basename } from "node:path"
3
3
  import { CliError, bool, c, confirm, defineCommand, info, log, prompt, str, success, warn } from "../cli"
4
4
  import { getCommits, getProject, publishProject, sync, uploadBlob, type Project, type SyncFileInput, type SyncFolderInput, type SyncResponse } from "../platform/api"
5
5
  import { computeServerDiff } from "../platform/serverDiff"
6
- import { classifyKind, diffLocal, hasChanges, isTextKind, scanProject } from "../project/localFiles"
6
+ import { diffLocal, hasChanges, isTextKind, scanProject } from "../project/localFiles"
7
7
  import { writeManifest, type FileKind, type Manifest, type ManifestFile } from "../project/manifest"
8
8
  import { buildPaths } from "../project/materialize"
9
9
  import { qrUrl, reportRelease, withSchemaConfirm } from "./releases"
@@ -107,7 +107,7 @@ export const runPush = async (opts: PushOptions): Promise<void> => {
107
107
  let uploaded = 0
108
108
  for (const file of scan.files) {
109
109
  const source = sourceFor(file.path)
110
- const kind: FileKind = source?.type ?? classifyKind(basename(file.path))
110
+ const kind: FileKind = source?.type ?? file.kind
111
111
  const entry: SyncFileInput = { path: file.path, type: kind }
112
112
  if (source) entry.assetId = source.id
113
113
  if (isTextKind(kind)) entry.text = readFileSync(file.absPath, "utf8")
@@ -9,6 +9,7 @@ import { buildWeb } from "../web/build"
9
9
  import { startPreview } from "../web/preview"
10
10
  import { bundleFlags } from "./shared"
11
11
  import { lanCandidates, loadDevToken } from "./dev"
12
+ import { devCertificate } from "../dev/https"
12
13
 
13
14
  /*
14
15
  * `lecodes web` — the project as a WEB PAGE (Web Lite: DOM and CSS, the browser's layout, text and
@@ -50,26 +51,31 @@ const kb = (bytes: number): string => bytes < 1024 ? `${bytes} B` : bytes < 1024
50
51
  const dev = defineCommand({
51
52
  name: "dev",
52
53
  summary: "Open the project as a web page, reloaded on every save",
53
- description: "Serves the project's bundle and assets with the page of Web Lite around them; a save recompiles and runs the app again, the page's console output and errors stream back here. The address's hash is the route the app is opened with (#/product/42 → app.launchUrl).",
54
+ description: "Serves the project's bundle and assets with the page of Web Lite around them; a save recompiles and runs the app again, the page's console output and errors stream back here. The address's hash is the route the app is opened with (#/product/42 → app.launchUrl). A web half of the project's own (app.json `\"ar\": { \"web\": \"./plugins/ar-8thwall\" }`) is built with every compile and served beside the page, its `web.static` folder from the project.\n\n`--https`: the page over https, for a phone's camera and motion sensors (a browser gives them to a secure page only) — a self-signed certificate for localhost and this machine's LAN addresses, made once and kept in ~/.lecodes/dev-https/. The phone's browser warns about it: accept it once.",
54
55
  flags: {
55
56
  port: num("server port", { default: DEFAULT_PORT }),
56
57
  entry: bundleFlags.entry,
58
+ https: bool("serve over https with a self-signed certificate (a phone's camera and sensors need it)"),
57
59
  "no-open": bool("don't open the browser"),
58
60
  },
59
- examples: ["lecodes web dev", "lecodes web dev --port 8080 --no-open"],
61
+ examples: ["lecodes web dev", "lecodes web dev --port 8080 --no-open", "lecodes web dev --https"],
60
62
  run: async ({ flags }) => {
61
63
  const { root, manifest, name } = project()
62
64
  const lite = litePage()
63
65
 
64
66
  const port = flags.port || DEFAULT_PORT
65
67
  // The page is opened on this machine; a phone on the same network reaches it by the LAN address.
66
- const lan = lanCandidates()[0]?.address
67
- const origin = `http://localhost:${port}`
68
+ const lans = lanCandidates().map((candidate) => candidate.address)
69
+ const lan = lans[0]
70
+ const scheme = flags.https ? "https" : "http"
71
+ const origin = `${scheme}://localhost:${port}`
72
+ const tls = flags.https ? await devCertificate(lans) : undefined
68
73
  const server = await startDevServer({
69
74
  root, manifest, name, origin, port,
70
75
  token: loadDevToken(root),
71
76
  entryOverride: flags.entry,
72
77
  lite,
78
+ tls,
73
79
  log: note,
74
80
  }).catch((e: unknown) => {
75
81
  const msg = e instanceof Error ? e.message : String(e)
@@ -95,7 +101,8 @@ const dev = defineCommand({
95
101
  log(`${c.bold("LeCodes Web")} — ${name}`)
96
102
  log("")
97
103
  log(`In the browser: ${c.bold(url)}`)
98
- if (lan) note(`On this network: ${url.replace("http://localhost:", `http://${lan}:`)}`)
104
+ if (lan) note(`On this network: ${url.replace(`${scheme}://localhost:`, `${scheme}://${lan}:`)}`)
105
+ if (tls) note("The certificate is this machine's own (~/.lecodes/dev-https): a browser warns about it — accept it once per device.")
99
106
  log("")
100
107
  log(`Watching ${root} — save a file to reload. Ctrl+C to stop.`)
101
108
 
@@ -1,8 +1,7 @@
1
1
  import { readFileSync } from "node:fs"
2
- import { basename } from "node:path"
3
2
  import type { Project } from "../platform/api"
4
3
  import type { Manifest } from "../project/manifest"
5
- import { classifyKind, scanLocal } from "../project/localFiles"
4
+ import { scanLocal } from "../project/localFiles"
6
5
  import { loadIgnore } from "../project/ignore"
7
6
  import { buildPaths } from "../project/materialize"
8
7
  import type { CompileEntry } from "./projectCompile"
@@ -35,7 +34,7 @@ export const collectEntries = (
35
34
  const warnings: string[] = []
36
35
  const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest.files)) })
37
36
  for (const file of scan.files) {
38
- const type = manifest.files[file.path]?.type ?? classifyKind(basename(file.path))
37
+ const type = manifest.files[file.path]?.type ?? file.kind
39
38
  if (type === "resource") {
40
39
  const src = serverByPath.get(file.path)?.fileSrc
41
40
  if (!src && (opts.warnMissingUrl ?? true)) warnings.push(`No server URL for ${file.path} — push it so its URL resolves.`)
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs"
2
2
  import { basename } from "node:path"
3
3
  import type { Manifest } from "../project/manifest"
4
- import { classifyKind, scanLocal } from "../project/localFiles"
4
+ import { scanLocal } from "../project/localFiles"
5
5
  import { loadIgnore } from "../project/ignore"
6
6
  import type { CompileEntry } from "./projectCompile"
7
7
 
@@ -13,6 +13,10 @@ import type { CompileEntry } from "./projectCompile"
13
13
  * in the artifacts; callers that don't pass `localShaders` keep the old degradation — an empty
14
14
  * artifact list + a push-once warning (same as `collectEntries` reports for unpushed shaders).
15
15
  */
16
+ const isHalfManifest = (abs: string): boolean => {
17
+ try { return typeof (JSON.parse(readFileSync(abs, "utf8").replace(/^/, "")) as { implements?: unknown }).implements === "string" } catch { return false }
18
+ }
19
+
16
20
  export const collectLocalEntries = (
17
21
  root: string, manifest: Manifest | null, assetBase: string,
18
22
  opts: { localShaders?: boolean } = {},
@@ -20,8 +24,15 @@ export const collectLocalEntries = (
20
24
  const entries: CompileEntry[] = []
21
25
  const warnings: string[] = []
22
26
  const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) })
27
+ // A HALF FOLDER (a manifest that `implements` a first-party plugin) is no part of the app: its
28
+ // app side is the SDK's wrapper, its files are a platform's half and what that half fetches (a
29
+ // runtime of megabytes) — never read into a compile, never a candidate entry.
30
+ const halfFolders = scan.files
31
+ .filter((f) => basename(f.path) === "lecodes-plugin.json" && isHalfManifest(f.absPath))
32
+ .map((f) => f.path.slice(0, -"lecodes-plugin.json".length))
23
33
  for (const file of scan.files) {
24
- const type = manifest?.files[file.path]?.type ?? classifyKind(basename(file.path))
34
+ if (halfFolders.some((dir) => file.path.startsWith(dir))) continue
35
+ const type = manifest?.files[file.path]?.type ?? file.kind
25
36
  if (type === "resource") {
26
37
  entries.push({ path: file.path, type, fileSrc: assetBase + encodeURI(file.path), absPath: file.absPath })
27
38
  } else if (type === "shader") {
@@ -1,7 +1,6 @@
1
1
  import { readFileSync } from "node:fs"
2
- import { basename } from "node:path"
3
2
  import type { Manifest } from "../project/manifest"
4
- import { classifyKind, scanLocal } from "../project/localFiles"
3
+ import { scanLocal } from "../project/localFiles"
5
4
  import { loadIgnore } from "../project/ignore"
6
5
  import { compileProject, type CompileEntry } from "./projectCompile"
7
6
  import { applyAssetIcons } from "./assetIcons"
@@ -44,7 +43,7 @@ export const compileDesignScreen = async (opts: {
44
43
  const entries: CompileEntry[] = []
45
44
  const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) })
46
45
  for (const file of scan.files) {
47
- const type = manifest?.files[file.path]?.type ?? classifyKind(basename(file.path))
46
+ const type = manifest?.files[file.path]?.type ?? file.kind
48
47
  if (type === "resource") {
49
48
  entries.push({
50
49
  path: file.path,
@@ -25,6 +25,9 @@ const resolveSdkInject = (): string | null => {
25
25
  }
26
26
  const sdkInject = resolveSdkInject()
27
27
  if (sdkInject) process.env.LECODES_SDK_INJECT = sdkInject
28
+ /** The SDK's source tree projects compile against (null: none found) — where the first-party
29
+ * plugins' contracts are read from too (plugin/firstParty.ts). */
30
+ export const sdkSourceDir: string | null = sdkInject ? dirname(sdkInject) : null
28
31
 
29
32
  /** Absolute path to the chisel binary: env override → the @letary/chisel npm package → a chisel
30
33
  * binary shipped next to the executable (standalone binary distribution). Null when none found. */