{"_id":"@multisetai/vps","_rev":"24-b815077625cbe1406d666b0e2732f687","name":"@multisetai/vps","dist-tags":{"latest":"2.4.1"},"versions":{"1.1.0":{"name":"@multisetai/vps","version":"1.1.0","keywords":["multiset","webxr","three","ar","localization","vps","visual-positioning","augmented-reality"],"author":"","license":"UNLICENSED","_id":"@multisetai/vps@1.1.0","maintainers":[{"name":"nikhilsawlani","email":"support@multiset.ai"},{"name":"shub3dfe","email":"shubham@multiset.ai"}],"homepage":"https://github.com/MultiSet-AI/multiset-vps-webxr#readme","bugs":{"url":"https://github.com/MultiSet-AI/multiset-vps-webxr/issues"},"dist":{"shasum":"180d23a457105be5a403b34c2a8a8a91e25b398a","tarball":"https://registry.npmjs.org/@multisetai/vps/-/vps-1.1.0.tgz","fileCount":9,"integrity":"sha512-WdkJJiXl4gUjzBipgTVVkErwDV21W5dzLYCbjiPbrDL4Fc2860I9GLIN8Htf8wNWm39mDktMjDObiz0EF1xphw==","signatures":[{"sig":"MEQCIBbrACPq0vfNPlMOCjdpE3iBhR3uJLAOIhU2WTL3xFJ/AiBvvycOpNoTPqEq4Uh0EEK4ly3xThoDSmPdBYTG2Qo8+A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":70577},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js","require":"./dist/core/index.js"},"./webxr":{"types":"./dist/webxr/index.d.ts","import":"./dist/webxr/index.js","require":"./dist/webxr/index.js"}},"gitHead":"90ad344ca9f4b065b2798e0d4affe2816e70080a","scripts":{"build":"tsup"},"_npmUser":{"name":"shub3dfe","email":"shubham@multiset.ai"},"repository":{"url":"git+https://github.com/MultiSet-AI/multiset-vps-webxr.git","type":"git"},"_npmVersion":"10.9.2","description":"Multiset VPS WebXR SDK - Core client and WebXR controller.","directories":{},"sideEffects":false,"_nodeVersion":"22.14.0","dependencies":{"axios":"1.10.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.1.0","eslint":"^9.25.0","rimraf":"^6.0.1","typescript":"~5.8.3","@types/three":"^0.176.0","eslint-config-prettier":"^9.1.0","@typescript-eslint/parser":"^8.30.1","@typescript-eslint/eslint-plugin":"^8.30.1"},"peerDependencies":{"three":">=0.176.0"},"_npmOperationalInternal":{"tmp":"tmp/vps_1.1.0_1775073374569_0.246189017981995","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@multisetai/vps","version":"2.0.0","keywords":["multiset","webxr","three","ar","localization","vps","visual-positioning","augmented-reality"],"author":"","license":"UNLICENSED","_id":"@multisetai/vps@2.0.0","maintainers":[{"name":"nikhilsawlani","email":"support@multiset.ai"},{"name":"shub3dfe","email":"shubham@multiset.ai"}],"homepage":"https://github.com/MultiSet-AI/multiset-vps-webxr#readme","bugs":{"url":"https://github.com/MultiSet-AI/multiset-vps-webxr/issues"},"dist":{"shasum":"fb52325363037965590fe24532c812b8af15d3b9","tarball":"https://registry.npmjs.org/@multisetai/vps/-/vps-2.0.0.tgz","fileCount":9,"integrity":"sha512-5BoQ7C/myJVLmgQK0reo7wA6R6+Rn8LAXJYnxFm6zyJnEcAxcmghMRLyC4tsGVUF+BfBc+YsZk/AniEveD8Baw==","signatures":[{"sig":"MEUCIQDZqHD7cDOO7nzsNc1jZXFhkjG/QDuP4X8z2/v4ZO/mOgIgQaJaOSDz2NANAFzj/5DpONpqsWxcs3oOhwLWjKfUOI8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":86395},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js","require":"./dist/core/index.js"},"./three":{"types":"./dist/three/index.d.ts","import":"./dist/three/index.js","require":"./dist/three/index.js"}},"gitHead":"7089d4a0c1a605d5c244168401713a34945b9368","scripts":{"build":"tsup"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:48585bdb-8bbe-4d1a-8c4f-238ee3cd4457"}},"repository":{"url":"git+https://github.com/MultiSet-AI/multiset-vps-webxr.git","type":"git"},"_npmVersion":"11.11.0","description":"Multiset VPS WebXR SDK - Core client and WebXR controller.","directories":{},"sideEffects":false,"_nodeVersion":"24.14.1","dependencies":{"axios":"1.10.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.1.0","eslint":"^9.25.0","rimraf":"^6.0.1","typescript":"~5.8.3","@types/three":"^0.176.0","eslint-config-prettier":"^9.1.0","@typescript-eslint/parser":"^8.30.1","@typescript-eslint/eslint-plugin":"^8.30.1"},"peerDependencies":{"three":">=0.176.0"},"_npmOperationalInternal":{"tmp":"tmp/vps_2.0.0_1778529695441_0.9512922213195125","host":"s3://npm-registry-packages-npm-production"}},"2.1.0":{"name":"@multisetai/vps","version":"2.1.0","keywords":["multiset","webxr","three","ar","localization","vps","visual-positioning","augmented-reality"],"author":"","license":"UNLICENSED","_id":"@multisetai/vps@2.1.0","maintainers":[{"name":"nikhilsawlani","email":"support@multiset.ai"},{"name":"shub3dfe","email":"shubham@multiset.ai"}],"homepage":"https://github.com/MultiSet-AI/multiset-vps-webxr#readme","bugs":{"url":"https://github.com/MultiSet-AI/multiset-vps-webxr/issues"},"dist":{"shasum":"95dd0545a91bd58305f7950e7e44dee429870c75","tarball":"https://registry.npmjs.org/@multisetai/vps/-/vps-2.1.0.tgz","fileCount":9,"integrity":"sha512-WK7AFy7CtaywsaFjSzvwtDIlZ+egjf7JYQ1nCJBCWsV3EossbpNBC7LgdCk1cxCzAtpWSyQCoqDu/YgfDTGfig==","signatures":[{"sig":"MEYCIQDfxMY6TzeKPHLaUOVlr0mKFcingpQAFzt0ACZGZbcipQIhANcGP9HzqI1X2vbu6ArJlKSE4XsTH6bf/K8/jil8fDb9","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":86420},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js","require":"./dist/core/index.js"},"./three":{"types":"./dist/three/index.d.ts","import":"./dist/three/index.js","require":"./dist/three/index.js"}},"gitHead":"0e2b7fd344453455c0e6b751383d66fe1de33554","scripts":{"build":"tsup"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:48585bdb-8bbe-4d1a-8c4f-238ee3cd4457"}},"repository":{"url":"git+https://github.com/MultiSet-AI/multiset-vps-webxr.git","type":"git"},"_npmVersion":"11.11.0","description":"Multiset VPS WebXR SDK - Core client and WebXR controller.","directories":{},"sideEffects":false,"_nodeVersion":"24.14.1","dependencies":{"axios":"1.10.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.1.0","eslint":"^9.25.0","rimraf":"^6.0.1","typescript":"~5.8.3","@types/three":"^0.176.0","eslint-config-prettier":"^9.1.0","@typescript-eslint/parser":"^8.30.1","@typescript-eslint/eslint-plugin":"^8.30.1"},"peerDependencies":{"three":">=0.176.0"},"_npmOperationalInternal":{"tmp":"tmp/vps_2.1.0_1778531553173_0.6761553100186521","host":"s3://npm-registry-packages-npm-production"}},"2.1.1":{"name":"@multisetai/vps","version":"2.1.1","keywords":["multiset","webxr","three","ar","localization","vps","visual-positioning","augmented-reality"],"author":"","license":"UNLICENSED","_id":"@multisetai/vps@2.1.1","maintainers":[{"name":"nikhilsawlani","email":"support@multiset.ai"},{"name":"shub3dfe","email":"shubham@multiset.ai"}],"homepage":"https://github.com/MultiSet-AI/multiset-vps-webxr#readme","bugs":{"url":"https://github.com/MultiSet-AI/multiset-vps-webxr/issues"},"dist":{"shasum":"87e49b99ab17c4d835471becc8b425584cfafb01","tarball":"https://registry.npmjs.org/@multisetai/vps/-/vps-2.1.1.tgz","fileCount":9,"integrity":"sha512-9E74a2QIaA7v+JvC6NQffJfShKlWHpmFRRMHP0bsFG/XUpz5TNmx+rUtpB/FcZ8t9IsRCAX53dX0ij2sATD00g==","signatures":[{"sig":"MEUCIQCaI3Y/bjVkd6hm+2jY1bugIdo38clQNObHoaamDXOHlQIgIjNkGleRgvmCzFLZBDDkFFy2MK5Yz28RP9wUOmgeyag=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":86531},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js","require":"./dist/core/index.js"},"./three":{"types":"./dist/three/index.d.ts","import":"./dist/three/index.js","require":"./dist/three/index.js"}},"gitHead":"101da86fa36ee0cc5472519378e45e75f0d05160","scripts":{"build":"tsup"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:48585bdb-8bbe-4d1a-8c4f-238ee3cd4457"}},"repository":{"url":"git+https://github.com/MultiSet-AI/multiset-vps-webxr.git","type":"git"},"_npmVersion":"11.11.0","description":"Multiset VPS WebXR SDK - Core client and WebXR controller.","directories":{},"sideEffects":false,"_nodeVersion":"24.14.1","dependencies":{"axios":"1.10.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.1.0","eslint":"^9.25.0","rimraf":"^6.0.1","typescript":"~5.8.3","@types/three":"^0.176.0","eslint-config-prettier":"^9.1.0","@typescript-eslint/parser":"^8.30.1","@typescript-eslint/eslint-plugin":"^8.30.1"},"peerDependencies":{"three":">=0.176.0"},"_npmOperationalInternal":{"tmp":"tmp/vps_2.1.1_1778605535018_0.7967524028870299","host":"s3://npm-registry-packages-npm-production"}},"2.2.0":{"name":"@multisetai/vps","version":"2.2.0","keywords":["multiset","webxr","three","ar","localization","vps","visual-positioning","augmented-reality"],"author":"","license":"UNLICENSED","_id":"@multisetai/vps@2.2.0","maintainers":[{"name":"nikhilsawlani","email":"support@multiset.ai"},{"name":"shub3dfe","email":"shubham@multiset.ai"}],"homepage":"https://github.com/MultiSet-AI/multiset-vps-webxr#readme","bugs":{"url":"https://github.com/MultiSet-AI/multiset-vps-webxr/issues"},"dist":{"shasum":"160efb316ed7f565c651fbbf0fed6c1e1ea8905e","tarball":"https://registry.npmjs.org/@multisetai/vps/-/vps-2.2.0.tgz","fileCount":9,"integrity":"sha512-yEQgERyzhmlrMtT0v+4P3cWmb235SvGqArejYaOwtnxWA2cLmziFP+BGvf8oYYFBa7pnoOW63P6LRb9CcNjdlQ==","signatures":[{"sig":"MEUCIQCY5Y45skXxE65NtrY1JKNtPitYjIAO2F4kNm6hqXsg4wIgEBVe3eyDiLVLnzB7fn5ErlZFwA9O6DF7cqWRmXjjflk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":121455},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js","require":"./dist/core/index.js"},"./three":{"types":"./dist/three/index.d.ts","import":"./dist/three/index.js","require":"./dist/three/index.js"}},"gitHead":"92981dcfd906c90e9bdd126e9b2a9428997bac61","scripts":{"build":"tsup"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:48585bdb-8bbe-4d1a-8c4f-238ee3cd4457"}},"repository":{"url":"git+https://github.com/MultiSet-AI/multiset-vps-webxr.git","type":"git"},"_npmVersion":"11.13.0","description":"Multiset VPS WebXR SDK - Core client and WebXR controller.","directories":{},"sideEffects":false,"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.1.0","eslint":"^9.25.0","rimraf":"^6.0.1","typescript":"~5.8.3","@types/three":"^0.176.0","eslint-config-prettier":"^9.1.0","@typescript-eslint/parser":"^8.30.1","@typescript-eslint/eslint-plugin":"^8.30.1"},"peerDependencies":{"three":">=0.176.0"},"_npmOperationalInternal":{"tmp":"tmp/vps_2.2.0_1780073519320_0.04979567011440378","host":"s3://npm-registry-packages-npm-production"}},"2.2.1":{"name":"@multisetai/vps","version":"2.2.1","keywords":["multiset","webxr","three","ar","localization","vps","visual-positioning","augmented-reality"],"author":"","license":"UNLICENSED","_id":"@multisetai/vps@2.2.1","maintainers":[{"name":"nikhilsawlani","email":"support@multiset.ai"},{"name":"shub3dfe","email":"shubham@multiset.ai"}],"homepage":"https://github.com/MultiSet-AI/multiset-vps-webxr#readme","bugs":{"url":"https://github.com/MultiSet-AI/multiset-vps-webxr/issues"},"dist":{"shasum":"978645f161cc78c0b21485f3f02f49484f369162","tarball":"https://registry.npmjs.org/@multisetai/vps/-/vps-2.2.1.tgz","fileCount":9,"integrity":"sha512-W+0nDRIAfKJPX+eu5G+jj/ddYake9wL/mQnUlxA0u9IzB9z9w7Z98hdA92nz8S+5Vwbhh/d7aIHdJW/SG80D3w==","signatures":[{"sig":"MEUCIDd9rc0JoWzxjVFXhnOQKJKd8dk4ALwcCVqBaCaQCzaSAiEAuu3FLxBSvFMnvg13tEb0vmjvnPPmkEqIO+o+oPaPbCs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":124640},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js","require":"./dist/core/index.js"},"./three":{"types":"./dist/three/index.d.ts","import":"./dist/three/index.js","require":"./dist/three/index.js"}},"gitHead":"278cb590824460e123bc750513b605b082ff1192","scripts":{"build":"tsup"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:48585bdb-8bbe-4d1a-8c4f-238ee3cd4457"}},"repository":{"url":"git+https://github.com/MultiSet-AI/multiset-vps-webxr.git","type":"git"},"_npmVersion":"11.13.0","description":"Multiset VPS WebXR SDK - Core client and WebXR controller.","directories":{},"sideEffects":false,"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.1.0","eslint":"^9.25.0","rimraf":"^6.0.1","typescript":"~5.8.3","@types/three":"^0.176.0","eslint-config-prettier":"^9.1.0","@typescript-eslint/parser":"^8.30.1","@typescript-eslint/eslint-plugin":"^8.30.1"},"peerDependencies":{"three":">=0.176.0"},"_npmOperationalInternal":{"tmp":"tmp/vps_2.2.1_1780084327053_0.8497626558809575","host":"s3://npm-registry-packages-npm-production"}},"2.3.1":{"name":"@multisetai/vps","version":"2.3.1","keywords":["multiset","webxr","three","ar","localization","vps","visual-positioning","augmented-reality"],"author":"","license":"UNLICENSED","_id":"@multisetai/vps@2.3.1","maintainers":[{"name":"nikhilsawlani","email":"support@multiset.ai"},{"name":"shub3dfe","email":"shubham@multiset.ai"}],"homepage":"https://github.com/MultiSet-AI/multiset-vps-webxr#readme","bugs":{"url":"https://github.com/MultiSet-AI/multiset-vps-webxr/issues"},"dist":{"shasum":"beb34ca677a78a20aa36b88f04272af54271a15e","tarball":"https://registry.npmjs.org/@multisetai/vps/-/vps-2.3.1.tgz","fileCount":15,"integrity":"sha512-4CZ1uiAathzdMK1nexsi7ICjBKaeV/FyJ8WWr+Jx3+akUiQ/5H3lKFhQ82pH0yky54Lvkz3+5Ds3LnVO7uVdBQ==","signatures":[{"sig":"MEYCIQC6h56AMcWsr3XWdVkweEZqSfJsKKHr0W37/noPKw9HQAIhALXYYGsPZhOD5OvYvz+OH1HL/gSjEVUipeMW3NG7TU6c","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":218426},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js","require":"./dist/core/index.js"},"./three":{"types":"./dist/three/index.d.ts","import":"./dist/three/index.js","require":"./dist/three/index.js"},"./needle":{"types":"./dist/needle/index.d.ts","import":"./dist/needle/index.js","require":"./dist/needle/index.js"}},"gitHead":"7701085d470f62587016eb6c42b7166ae9f305ca","scripts":{"build":"tsup"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:48585bdb-8bbe-4d1a-8c4f-238ee3cd4457"}},"repository":{"url":"git+https://github.com/MultiSet-AI/multiset-vps-webxr.git","type":"git"},"_npmVersion":"11.13.0","description":"Multiset VPS WebXR SDK - Core client and WebXR controller.","directories":{},"sideEffects":false,"_nodeVersion":"24.17.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.1.0","three":"npm:@needle-tools/three@0.169.19","eslint":"^9.25.0","rimraf":"^6.0.1","typescript":"~5.8.3","@types/three":"0.169.0","@needle-tools/engine":"5.1.2","eslint-config-prettier":"^9.1.0","@typescript-eslint/parser":"^8.30.1","@typescript-eslint/eslint-plugin":"^8.30.1"},"peerDependencies":{"three":">=0.169.0","@needle-tools/engine":">=5.1.2"},"peerDependenciesMeta":{"three":{"optional":true},"@needle-tools/engine":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/vps_2.3.1_1782800486382_0.7051996407200842","host":"s3://npm-registry-packages-npm-production"}},"2.3.3":{"name":"@multisetai/vps","version":"2.3.3","keywords":["multiset","webxr","three","ar","localization","vps","visual-positioning","augmented-reality"],"author":"","license":"UNLICENSED","_id":"@multisetai/vps@2.3.3","maintainers":[{"name":"nikhilsawlani","email":"support@multiset.ai"},{"name":"shub3dfe","email":"shubham@multiset.ai"}],"homepage":"https://github.com/MultiSet-AI/multiset-vps-webxr#readme","bugs":{"url":"https://github.com/MultiSet-AI/multiset-vps-webxr/issues"},"dist":{"shasum":"47cc0b6ff0ad80751b067f6a5dcd4a97fcd800b6","tarball":"https://registry.npmjs.org/@multisetai/vps/-/vps-2.3.3.tgz","fileCount":16,"integrity":"sha512-NSq+xV5ZI1I7Woh2qBDlx5Qffe+8Q/bmx8mFugWSLB6XTSUsH21GhSHuGJJH9StX5M8LSKZqto9t2QV1dfEiQg==","signatures":[{"sig":"MEUCIC/rBqGqafyPcbS5N7mPKkspy5LIb93cmirnFcP7MOQtAiEA0v0wDB2r5m6pqcDN8wdNJNAjcN4BG9uvXZlK7gSXra0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":241514},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js","require":"./dist/core/index.js"},"./three":{"types":"./dist/three/index.d.ts","import":"./dist/three/index.js","require":"./dist/three/index.js"},"./needle":{"types":"./dist/needle/index.d.ts","import":"./dist/needle/index.js","require":"./dist/needle/index.js"}},"gitHead":"d9c5b917af4deb5dad464a268da4a0b603815222","scripts":{"build":"tsup"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:48585bdb-8bbe-4d1a-8c4f-238ee3cd4457"}},"repository":{"url":"git+https://github.com/MultiSet-AI/multiset-vps-webxr.git","type":"git"},"_npmVersion":"11.16.0","description":"Multiset VPS WebXR SDK - Core client and WebXR controller.","directories":{},"sideEffects":false,"_nodeVersion":"24.18.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.1.0","three":"npm:@needle-tools/three@0.169.19","eslint":"^9.25.0","rimraf":"^6.0.1","typescript":"~5.8.3","@types/three":"0.169.0","@needle-tools/engine":"5.1.9","eslint-config-prettier":"^9.1.0","@typescript-eslint/parser":"^8.30.1","@typescript-eslint/eslint-plugin":"^8.30.1"},"peerDependencies":{"three":">=0.169.0","@needle-tools/engine":">=5.1.9"},"peerDependenciesMeta":{"three":{"optional":true},"@needle-tools/engine":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/vps_2.3.3_1786020551316_0.6028612212776645","host":"s3://npm-registry-packages-npm-production"}},"2.4.0":{"name":"@multisetai/vps","version":"2.4.0","keywords":["multiset","webxr","three","ar","localization","vps","visual-positioning","augmented-reality"],"author":"","license":"UNLICENSED","_id":"@multisetai/vps@2.4.0","maintainers":[{"name":"nikhilsawlani","email":"support@multiset.ai"},{"name":"shub3dfe","email":"shubham@multiset.ai"}],"homepage":"https://github.com/MultiSet-AI/multiset-vps-webxr#readme","bugs":{"url":"https://github.com/MultiSet-AI/multiset-vps-webxr/issues"},"dist":{"shasum":"5373c2088bc6b1662920c303c28ff70a8cf787ed","tarball":"https://registry.npmjs.org/@multisetai/vps/-/vps-2.4.0.tgz","fileCount":20,"integrity":"sha512-/Tebzb2e304L9u0JBmPXCWkI9BjNJKZt5EFRZaEGeestGOBahH85Rda34nr8Y27/8Tmfi/vYtp2PLiS0AJEI+Q==","signatures":[{"sig":"MEUCIGE5o1oLX2f+VzW2klbiZNpUeX1ss5Bif8VpUhqeBbgTAiEA+koMEdwpW2wxUt+CgQ9FSVY83xMan2lRqV0NW4NMAVY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":319417},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js","require":"./dist/core/index.js"},"./three":{"types":"./dist/three/index.d.ts","import":"./dist/three/index.js","require":"./dist/three/index.js"},"./needle":{"types":"./dist/needle/index.d.ts","import":"./dist/needle/index.js","require":"./dist/needle/index.js"},"./navigation":{"types":"./dist/navigation/index.d.ts","import":"./dist/navigation/index.js","require":"./dist/navigation/index.js"}},"gitHead":"0fb8cf4a5a610d7accbed287845413f5da20b96e","scripts":{"build":"tsup"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:48585bdb-8bbe-4d1a-8c4f-238ee3cd4457"}},"repository":{"url":"git+https://github.com/MultiSet-AI/multiset-vps-webxr.git","type":"git"},"_npmVersion":"11.17.0","description":"Multiset VPS WebXR SDK - Core client and WebXR controller.","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"typesVersions":{"*":{"core":["./dist/core/index.d.ts"],"three":["./dist/three/index.d.ts"],"needle":["./dist/needle/index.d.ts"],"navigation":["./dist/navigation/index.d.ts"]}},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.1.0","three":"npm:@needle-tools/three@0.169.19","eslint":"^9.25.0","rimraf":"^6.0.1","typescript":"~5.8.3","@types/three":"0.169.0","three-pathfinding":"^1.3.0","@needle-tools/engine":"5.1.9","eslint-config-prettier":"^9.1.0","@typescript-eslint/parser":"^8.30.1","@typescript-eslint/eslint-plugin":"^8.30.1"},"peerDependencies":{"three":">=0.169.0","three-pathfinding":">=1.3.0","@needle-tools/engine":">=5.1.9"},"peerDependenciesMeta":{"three":{"optional":true},"three-pathfinding":{"optional":true},"@needle-tools/engine":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/vps_2.4.0_1786705889003_0.44713927334322734","host":"s3://npm-registry-packages-npm-production"}},"2.4.1":{"name":"@multisetai/vps","version":"2.4.1","description":"Multiset VPS WebXR SDK - Core client and WebXR controller.","type":"module","main":"dist/index.js","module":"dist/index.js","types":"dist/index.d.ts","sideEffects":false,"scripts":{"build":"tsup"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js","require":"./dist/core/index.js"},"./three":{"types":"./dist/three/index.d.ts","import":"./dist/three/index.js","require":"./dist/three/index.js"},"./needle":{"types":"./dist/needle/index.d.ts","import":"./dist/needle/index.js","require":"./dist/needle/index.js"},"./navigation":{"types":"./dist/navigation/index.d.ts","import":"./dist/navigation/index.js","require":"./dist/navigation/index.js"}},"typesVersions":{"*":{"core":["./dist/core/index.d.ts"],"three":["./dist/three/index.d.ts"],"needle":["./dist/needle/index.d.ts"],"navigation":["./dist/navigation/index.d.ts"]}},"keywords":["multiset","webxr","three","ar","localization","vps","visual-positioning","augmented-reality"],"engines":{"node":">=18"},"author":"","license":"UNLICENSED","repository":{"type":"git","url":"git+https://github.com/MultiSet-AI/multiset-vps-webxr.git"},"publishConfig":{"access":"public"},"peerDependencies":{"@needle-tools/engine":">=5.1.9","three":">=0.169.0","three-pathfinding":">=1.3.0"},"peerDependenciesMeta":{"three":{"optional":true},"@needle-tools/engine":{"optional":true},"three-pathfinding":{"optional":true}},"devDependencies":{"@needle-tools/engine":"5.1.9","@types/three":"0.169.0","@typescript-eslint/eslint-plugin":"^8.30.1","@typescript-eslint/parser":"^8.30.1","eslint":"^9.25.0","eslint-config-prettier":"^9.1.0","rimraf":"^6.0.1","three":"npm:@needle-tools/three@0.169.19","three-pathfinding":"^1.3.0","tsup":"^8.1.0","typescript":"~5.8.3"},"gitHead":"3248eba9ece9e290c60abb3e18facf4c81563503","_id":"@multisetai/vps@2.4.1","bugs":{"url":"https://github.com/MultiSet-AI/multiset-vps-webxr/issues"},"homepage":"https://github.com/MultiSet-AI/multiset-vps-webxr#readme","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-itUSMq9wQd7TyAeNYKFyKKAvJWx3ME/lgU/sdd6IzxZSCtPsGvOqN+Un6JWt3e0zDdciqyNPQrLSy4o65Y8jIg==","shasum":"25453e98069f0da7346dc42b89d33b10b6114d73","tarball":"https://registry.npmjs.org/@multisetai/vps/-/vps-2.4.1.tgz","fileCount":20,"unpackedSize":319625,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIHyX+z0uBmQ0KkSya/kPaSG36pT5Y7Ksa3ylxZvs7kTfAiEAryTsajHT/ujRKOyzn4vZnALC+S7t+e7otbyqwNutHpg="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:48585bdb-8bbe-4d1a-8c4f-238ee3cd4457"}},"directories":{},"maintainers":[{"name":"nikhilsawlani","email":"support@multiset.ai"},{"name":"shub3dfe","email":"shubham@multiset.ai"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/vps_2.4.1_1787127177584_0.4989844743087357"},"_hasShrinkwrap":false}},"time":{"created":"2025-12-09T17:54:58.891Z","modified":"2026-08-19T08:12:57.976Z","1.0.4":"2025-12-09T17:54:59.126Z","1.0.5":"2026-01-04T16:27:48.245Z","1.0.6":"2026-01-09T10:42:49.036Z","1.0.7-beta.0":"2026-02-27T19:40:20.611Z","1.0.7-beta.1":"2026-03-03T20:25:46.462Z","1.0.7-beta.2":"2026-03-04T19:06:13.632Z","1.0.7-beta.3":"2026-03-10T19:38:05.741Z","1.0.7-beta.4":"2026-03-10T20:02:42.288Z","1.0.7-beta.5":"2026-03-11T18:42:23.131Z","1.0.7-beta.6":"2026-03-11T18:55:39.203Z","1.1.0":"2026-04-01T19:56:14.724Z","2.0.0":"2026-05-11T20:01:35.634Z","2.1.0":"2026-05-11T20:32:33.317Z","2.1.1":"2026-05-12T17:05:35.228Z","2.2.0":"2026-05-29T16:51:59.454Z","2.2.1":"2026-05-29T19:52:07.196Z","2.3.1":"2026-06-30T06:21:26.576Z","2.3.3":"2026-08-06T12:49:11.439Z","2.4.0":"2026-08-14T11:11:29.165Z","2.4.1":"2026-08-19T08:12:57.785Z"},"bugs":{"url":"https://github.com/MultiSet-AI/multiset-vps-webxr/issues"},"license":"UNLICENSED","homepage":"https://github.com/MultiSet-AI/multiset-vps-webxr#readme","keywords":["multiset","webxr","three","ar","localization","vps","visual-positioning","augmented-reality"],"repository":{"type":"git","url":"git+https://github.com/MultiSet-AI/multiset-vps-webxr.git"},"description":"Multiset VPS WebXR SDK - Core client and WebXR controller.","maintainers":[{"name":"nikhilsawlani","email":"support@multiset.ai"},{"name":"shub3dfe","email":"shubham@multiset.ai"}],"readme":"# MultiSet VPS WebXR\n\nTypeScript SDK for integrating MultiSet's Visual Positioning System (VPS) into WebXR applications. Provides 6-DOF localization by matching camera frames against cloud-hosted maps, and object tracking by matching camera frames against registered 3D objects.\n\n## Contents\n\n- [Architecture](#architecture)\n- [Installation](#installation)\n- [Requirements](#requirements)\n- [CORS Configuration](#cors-configuration)\n- [Quick Start](#quick-start)\n  - [VPS — Three.js](#vps-localization--threejs)\n  - [VPS — Vanilla / WebGL2](#vps-localization--without-threejs-webgl2--vanilla)\n  - [Object Tracking — Three.js](#object-tracking--threejs)\n  - [Needle Engine](#needle-engine)\n    - [Unity Inspector Workflow](#unity-inspector-workflow)\n    - [MapSpace — anchoring a whole subtree](#mapspace--anchoring-a-whole-subtree-unity)\n    - [MapAnchor — zero-code object placement](#mapanchor--zero-code-object-placement-unity)\n- [Navigation](#navigation) — AR wayfinding ([full guide](https://docs.multiset.ai/multiset/webxr-sdk/navigation))\n- [Canvas Visibility During AR](#canvas-visibility-during-ar)\n- [API Reference](#api-reference)\n  - [MultisetClient](#multisetclient)\n  - [XRSessionManager](#xrsessionmanager)\n  - [ThreeAdapter](#threeadapter)\n  - [NeedleAdapter](#needleadapter)\n  - [IVpsAdapter](#ivpsadapter) — the contract both adapters share\n  - [MapSpace](#mapspace)\n  - [MapAnchor](#mapanchor)\n  - [Navigation](#navigation-1)\n  - [NavMeshPathfinder](#navmeshpathfinder)\n  - [buildPathRibbon](#buildpathribboncorners-options)\n  - [MultisetVPS](#multisetvps)\n- [Placing Content at Map Coordinates](#placing-content-at-map-coordinates)\n- [Styling the AR Button](#styling-the-ar-button)\n- [Custom Start / Stop Button](#custom-start--stop-button)\n- [Type Definitions](#type-definitions)\n- [Troubleshooting](#troubleshooting)\n  - [AR button doesn't appear](#ar-button-doesnt-appear)\n  - [MapAnchor not appearing after localization](#mapanchor-not-appearing-after-localization-needle)\n  - [MapSpace content missing or misplaced](#mapspace-content-not-appearing-or-in-the-wrong-place-needle)\n  - [Component start() never called](#component-start-never-called-needle)\n\n---\n\n## Architecture\n\nThe SDK is split into independent entry points so you only install what you need:\n\n| Entry point | Contents | Peer deps |\n|---|---|---|\n| `@multisetai/vps/core` | `MultisetClient` + `XRSessionManager` | None |\n| `@multisetai/vps/three` | `ThreeAdapter` | `three >=0.169.0` |\n| `@multisetai/vps/needle` | `NeedleAdapter` | `@needle-tools/engine` (brings its own `three`) |\n\n`XRSessionManager` owns the full vanilla WebXR session lifecycle — frame loop, camera capture, localization, tracking-loss recovery — with zero Three.js dependency. `ThreeAdapter` and `NeedleAdapter` wire it to a renderer and scene.\n\n## Installation\n\n```bash\n# Core only (no Three.js)\nnpm install @multisetai/vps\n\n# With Three.js adapter\nnpm install @multisetai/vps three\n\n# With Needle Engine — no separate `three` install needed.\n# Needle Engine ships its own three.js fork as a dependency.\nnpm install @multisetai/vps\n```\n\n## Requirements\n\n- **HTTPS** — WebXR requires a secure context (`https://` or `http://localhost`).\n- **Android + ARCore** — Chrome or Edge on an ARCore-capable Android device (Android 8+, Chrome 81+).\n- **Three.js ≥ 0.169.0** — only required when using `@multisetai/vps/three`. Compatible through the latest release (r184+).\n- **Needle Engine ≥ 5.0.0** — only required when using `@multisetai/vps/needle`. Needle ships its own `three` fork — do **not** install `three` separately.\n\n> **iOS is not supported.** This SDK requires the `camera-access` WebXR feature. Safari on iOS does not implement it.\n\n## CORS Configuration\n\nThe SDK makes direct browser-to-API requests, so your domain must be whitelisted in the MultiSet dashboard.\n\n1. Open the [MultiSet Dashboard](https://docs.multiset.ai/basics/credentials/configuring-allowed-domains-cors)\n2. Go to **Credentials → Settings → Domains**\n3. Click **Add +** and enter your origin (e.g. `https://localhost:5173` for dev, `https://your-app.com` for prod)\n\nWithout this, the browser will block every API request with a CORS error.\n\n---\n\n## Quick Start\n\n### VPS Localization — Three.js\n\n```typescript\nimport * as THREE from 'three';\nimport { MultisetClient, XRSessionManager } from '@multisetai/vps/core';\nimport { ThreeAdapter } from '@multisetai/vps/three';\n\n// Check support before showing any AR UI\nconst supported = await ThreeAdapter.isSupported();\nif (!supported) {\n  console.warn('WebXR immersive-ar is not supported on this device.');\n}\n\nconst client = new MultisetClient({\n  clientId: 'CLIENT_ID',\n  clientSecret: 'CLIENT_SECRET',\n  code: 'MAP_OR_MAPSET_CODE',\n  mapType: 'map',\n});\n\nawait client.authorize();\n\nconst renderer = new THREE.WebGLRenderer({ antialias: true });\ndocument.body.appendChild(renderer.domElement);\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 1000);\n\nconst session = new XRSessionManager(renderer.getContext() as WebGL2RenderingContext, {\n  client,\n  overlayRoot: document.body,\n  autoLocalize: true,\n  confidenceCheck: true,\n  confidenceThreshold: 0.5,\n  onSessionStart: () => {\n    // Hide the canvas during AR — the XR compositor owns the display.\n    renderer.domElement.style.display = 'none';\n  },\n  onSessionEnd: () => {\n    renderer.domElement.style.display = 'block';\n  },\n  onLocalizationResult: (result) => console.log('Localized:', result.localizeData.position),\n  onLocalizationFailure: (reason) => console.warn('Failed:', reason),\n  onError: (err) => console.error(err),\n});\n\nconst adapter = new ThreeAdapter({ session, renderer, scene, camera, showMesh: true });\nadapter.initialize(); // mounts the built-in START AR / STOP AR button\n\n// Add your 3D content\nscene.add(new THREE.Mesh(\n  new THREE.BoxGeometry(0.1, 0.1, 0.1),\n  new THREE.MeshBasicMaterial({ color: 0xff0077 })\n));\n```\n\n### VPS Localization — Without Three.js (WebGL2 / Vanilla)\n\nIn this mode the SDK manages the session lifecycle, camera capture, and localization. You are responsible for all rendering: each XR frame, draw your scene into `event.baseLayer.framebuffer` using the provided `gl` context. This approach works with any WebGL2-based renderer — Babylon.js, raw WebGL, or your own engine.\n\n```typescript\nimport { MultisetClient, XRSessionManager } from '@multisetai/vps/core';\n\nconst supported = await XRSessionManager.isSupported();\nif (!supported) {\n  console.warn('WebXR immersive-ar is not supported on this device.');\n}\n\nconst client = new MultisetClient({ clientId: '...', clientSecret: '...', code: '...', mapType: 'map' });\nawait client.authorize();\n\n// A WebGL2 context is required — WebXR renders into a GL framebuffer, not a 2D canvas.\nconst gl = document.querySelector('canvas')!.getContext('webgl2')!;\n\nconst session = new XRSessionManager(gl, {\n  client,\n  overlayRoot: document.body,\n  autoLocalize: true,\n  onLocalizationResult: (result) => console.log('Localized:', result.localizeData.position),\n  onError: (err) => console.error(err),\n});\n\n// Wire your render loop — called every XR frame with the current pose and framebuffer.\nsession.setXRFrameHandler((event) => {\n  // Bind event.baseLayer.framebuffer and render your scene using event.view for camera matrices.\n});\n\ndocument.body.appendChild(session.createButton());\n```\n\n### Object Tracking — Three.js\n\nObject tracking detects and poses registered 3D objects by matching a captured camera frame against the MultiSet cloud.\n\n```typescript\nimport * as THREE from 'three';\nimport { MultisetClient, XRSessionManager } from '@multisetai/vps/core';\nimport { ThreeAdapter } from '@multisetai/vps/three';\n\nconst client = new MultisetClient({\n  clientId: 'CLIENT_ID',\n  clientSecret: 'CLIENT_SECRET',\n  mapType: 'object-tracking',\n  code: ['YOUR_OBJECT_CODE'],\n});\n\nawait client.authorize();\n\nconst renderer = new THREE.WebGLRenderer({ antialias: true });\ndocument.body.appendChild(renderer.domElement);\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 1000);\n\nconst session = new XRSessionManager(renderer.getContext() as WebGL2RenderingContext, {\n  client,\n  overlayRoot: document.body,\n  autoTracking: true,          // detect once on session start\n  confidenceCheck: true,\n  confidenceThreshold: 0.5,\n  onSessionStart: () => { renderer.domElement.style.display = 'none'; },\n  onSessionEnd:   () => { renderer.domElement.style.display = 'block'; },\n  onObjectTrackingSuccess: (result) => {\n    console.log('Detected', result.objectCodes, 'at', result.position);\n  },\n  onObjectTrackingFailure: (reason) => console.warn('Tracking failed:', reason),\n  onError: (err) => console.error(err),\n});\n\nconst adapter = new ThreeAdapter({\n  session,\n  renderer,\n  scene,\n  camera,\n  showObjectMeshes: true,       // load and display the 3D outline mesh\n  onObjectMeshLoaded: (code) => console.log('Mesh loaded for', code),\n});\nadapter.initialize();\n\n// Trigger tracking manually from a button\ntrackButton.addEventListener('click', () => {\n  void adapter.trackObjects();\n});\n```\n\n### Needle Engine\n\n`NeedleAdapter` is a Needle Engine `Behaviour` component. Add it to a GameObject via `addNewComponent()` from your own component's `start()`. The built-in START AR / STOP AR button mounts automatically — no `initialize()` call needed.\n\n> **Important:** Do **not** add a Needle `WebXR` component to the same scene. `NeedleAdapter` must own the WebXR session directly because it bypasses Three.js's built-in XR manager (`renderer.xr`). This is required to work around a Chrome/WebXR regression where enabling `camera-access` corrupts Three.js's internal texture state. Adding a Needle `WebXR` component would start a second session and conflict with this setup. Delete the `WebXR` component from the scene hierarchy before adding `MultisetVPS`.\n\n#### Unity Inspector workflow\n\n`MultisetVPS`, `MapSpace`, and `MapAnchor` are ready-made components. Copy a few template files into your project, fill in credentials in the Inspector — no code required.\n\n> **Why template files?** Needle Engine's Unity codegen scans only your local `src/scripts/` folder to generate C# stubs — it skips `node_modules`. Copying the templates into your project gives Unity the component definitions it needs, while all SDK utilities (`NeedleAdapter`, `MultisetClient`, etc.) are still imported from the package.\n>\n> A consequence worth remembering: these components are **not package exports**. In code, import them relatively (`./MapSpace.js`), never from `@multisetai/vps/needle`. They are also yours to edit — Needle will not overwrite them.\n\n1. **Install** in your Needle web project:\n   ```bash\n   npm install @multisetai/vps\n   ```\n\n2. **Copy the TypeScript templates** into your Needle project's scripts folder (the folder Needle scans for components — `web/src/scripts/` by default, but use whatever path your project uses):\n\n   ```bash\n   cp node_modules/@multisetai/vps/templates/MultisetVPS.ts <your-web-folder>/src/scripts/\n   cp node_modules/@multisetai/vps/templates/MapSpace.ts    <your-web-folder>/src/scripts/\n   cp node_modules/@multisetai/vps/templates/MapAnchor.ts   <your-web-folder>/src/scripts/\n   cp node_modules/@multisetai/vps/templates/MapType.ts     <your-web-folder>/src/scripts/\n   ```\n\n   > `MapType.ts` must be present alongside `MultisetVPS.ts` — Needle's component compiler resolves the enum from this file to generate the **Map Type** dropdown. Without it the component will not appear in the Inspector.\n\n   Needle Engine automatically picks up everything in your scripts folder and generates the matching C# stubs in Unity.\n\n3. **Copy the C# enum** into your Unity project's `Assets/` folder (one-time setup — this file is never overwritten by Needle). Run this from inside your web folder:\n\n   ```bash\n   cp node_modules/@multisetai/vps/templates/MapType.cs ../Assets/\n   ```\n\n   This gives you a **Map Type** dropdown in the Inspector (`SingleMap / MapSet / ObjectTracking`). Without it, Needle generates the field but Unity can't compile the enum reference.\n\n4. **In Unity**, add a `MultisetVPS` component to any GameObject and fill in `clientId`, `clientSecret`, and `mapCode` in the Inspector.\n\n5. **Place your content.** Two options, and `MapSpace` is the recommended default:\n\n   - **`MapSpace`** — add it to a single empty GameObject at the scene root, then nest everything you want anchored underneath. Each child's normal Unity Transform position *is* its map coordinate, so you can paste values straight from the developer portal's Map Viewer and lay the scene out visually in the editor. See [MapSpace — anchoring a whole subtree](#mapspace--anchoring-a-whole-subtree-unity).\n   - **`MapAnchor`** — add it to an individual GameObject and type its coordinate into the `Offset` field. Best for one-off objects and runtime-spawned content. See [MapAnchor — zero-code object placement](#mapanchor--zero-code-object-placement-unity).\n\n   `MultisetVPS` discovers all `MapSpace` and `MapAnchor` instances automatically at startup — no manual wiring needed.\n\n6. **Export to web** — the START AR / STOP AR and CAPTURE FRAME buttons appear automatically.\n\n### Where map coordinates come from\n\nOpen your map in the **Map Viewer** (or **MapSet Viewer**) in the [developer portal](https://developer.multiset.ai), pick the point you want to anchor to, and copy the coordinates. These are **Unity left-handed** values, which is exactly what both components expect by default — paste them in unchanged.\n\nFor runtime-spawned objects (loaded from an API, instantiated mid-session), call `adapter.registerAnchor(anchor)` — see [MapAnchor — zero-code object placement](#mapanchor--zero-code-object-placement-unity) below.\n\n#### Code-based setup (without Unity Inspector)\n\n```typescript\nimport { Behaviour, serializable, addNewComponent } from '@needle-tools/engine';\nimport { MultisetClient } from '@multisetai/vps/core';\nimport { NeedleAdapter } from '@multisetai/vps/needle';\nimport * as THREE from 'three';\n\nexport class MyARComponent extends Behaviour {\n  async start() {\n    const client = new MultisetClient({\n      clientId: 'CLIENT_ID',\n      clientSecret: 'CLIENT_SECRET',\n      code: 'MAP_CODE',\n      mapType: 'map',\n    });\n\n    const adapter = new NeedleAdapter({\n      client,\n      showMesh: true,\n      sessionOptions: {\n        autoLocalize: true,\n        onSessionStart: () => console.log('AR started'),\n        onSessionEnd:   () => console.log('AR ended'),\n        onLocalizationFailure: (reason) => console.warn('Failed:', reason),\n        onError: (err) => console.error(err),\n      },\n      onLocalizationSuccess: (result, worldFromMap) => {\n        // Place content anchored to the scanned map\n        const marker = new THREE.Mesh(\n          new THREE.SphereGeometry(0.05),\n          new THREE.MeshBasicMaterial({ color: 0x00ff88 })\n        );\n        marker.position.applyMatrix4(worldFromMap);\n        this.context.scene.add(marker);\n      },\n    });\n\n    // addNewComponent triggers awake() — button mounts automatically\n    addNewComponent(this.gameObject, adapter);\n    await client.authorize();\n  }\n}\n```\n\n#### MapSpace — anchoring a whole subtree (Unity)\n\n`MapSpace` marks one GameObject as the **map origin**. On every successful localization it is moved so that its origin coincides with the scanned map's origin, and every descendant follows automatically.\n\nThis is the recommended way to place content in Unity, because a child's local Transform position *is* its map coordinate:\n\n```\nSampleScene\n├── MultisetVPS          ← credentials\n└── Map Space            ← MapSpace, transform at identity\n    ├── Office Entry     ← local position = portal coordinate\n    ├── Coffee Shack     ← local position = portal coordinate\n    └── Lift             ← local position = portal coordinate\n```\n\n**Setup**\n\n1. Create an empty GameObject at the scene root. Leave its transform at identity — Position `(0,0,0)`, Rotation `(0,0,0)`, Scale `(1,1,1)`.\n2. Add the `MapSpace` component.\n3. Nest the objects you want anchored underneath it.\n4. Set each child's normal Unity Transform position to its coordinate from the portal's Map Viewer.\n\n| Inspector field | Type | Default | Description |\n|---|---|---|---|\n| **Hide Until Localized** | `bool` | `true` | Hide anchored content until the first successful localization. Re-hides when the AR session ends. |\n\n**Why this over `MapAnchor` per object**\n\n- The Unity editor layout *is* the AR layout — arrange POIs visually instead of typing coordinates you cannot verify until you deploy.\n- Import the scanned map mesh as a child and place content against real geometry.\n- Relative layout is preserved exactly. Everything moves as one rigid body, so POIs can never drift apart.\n- One transform write per localization regardless of POI count.\n- No per-object handedness flag to get wrong.\n\n> **Note** — Unity authored transforms are converted to Three.js by Needle's exporter, so no handedness setting is involved. This is why `MapSpace` has no `isRightHanded` field while `MapAnchor` does.\n\n> **Important** — `hideUntilLocalized` makes the root invisible, and Needle treats an invisible GameObject as inactive. That applies to every **descendant**, so none of their `start()` methods run until the first localization. Keep always-running logic (session listeners, UI, network polling) on a separate GameObject **outside** this hierarchy.\n\n**Do not nest a `MapAnchor` inside a `MapSpace`.** `MapAnchor` writes a world position into a local transform, so inside an already-positioned root the offset is applied twice. `MultisetVPS` logs a warning if it detects this. Use one or the other for a given object.\n\n**Runtime-added children** do not go through the Unity exporter, so route their coordinates through `MapSpace.toLocal()`:\n\n```typescript\nimport { MapSpace } from './MapSpace.js';\nimport * as THREE from 'three';\n\nconst poi = instantiate(myPoiPrefab);\nmapSpace.gameObject.add(poi);\npoi.position.copy(MapSpace.toLocal(new THREE.Vector3(1.5, 0, -2.0)));  // Unity LHS in\n```\n\nAlternatively use `MapAnchor` with `registerAnchor()` and leave the object unparented — it accepts Unity values directly.\n\n---\n\n#### MapAnchor — zero-code object placement (Unity)\n\n`MapAnchor` is a Needle Engine component that anchors any GameObject to the VPS map origin after a successful localization. Add it to the object you want to place in AR from the Unity Inspector — no code required.\n\nUse it for individual objects, runtime-spawned content, and anything that must not inherit the map's rotation. For laying out several POIs in a scene, prefer [`MapSpace`](#mapspace--anchoring-a-whole-subtree-unity).\n\n| Inspector field | Type | Description |\n|---|---|---|\n| **Offset** | `Vector3` | Position offset from the map origin in metres. Enter Unity Inspector values directly — X is negated automatically unless **Is Right Handed** is enabled. |\n| **Match Orientation** | `bool` | Align the object's rotation to the map's orientation. |\n| **Rotation Offset** | `Vector3` (degrees) | Additional rotation applied on top of the map orientation. Only used when **Match Orientation** is enabled. Enter Unity Inspector values — Y and Z are negated automatically unless **Is Right Handed** is enabled. |\n| **Hide Until Localized** | `bool` | Hide the object until the first successful localization. Re-hides it when the AR session ends. |\n| **Is Right Handed** | `bool` | Off by default. When off, **Offset** and **Rotation Offset** are treated as Unity (left-handed) values and converted automatically. Enable only if you are entering Three.js (right-handed) values directly. |\n\n`MultisetVPS` (or your own component calling `addNewComponent`) automatically discovers all `MapAnchor` instances in the scene at startup and wires them to the adapter. You do not call any method manually.\n\n> **Important:** When `hideUntilLocalized` is `true`, Needle sets `visible = false` on the GameObject in `awake()`. An invisible GameObject is inactive in Needle — every component on it, not just `MapAnchor`, will have its `start()` skipped. Keep `MapAnchor` on the object you want placed in AR. Put any logic components (session listeners, custom UI) on a **separate, always-visible GameObject**.\n\nFor objects spawned **at runtime** (e.g. from API data or a prefab instantiated mid-session), register them explicitly so they receive localization events and are placed immediately if localization has already succeeded:\n\n```typescript\n// MapAnchor is a template file you copied into your own project — import it\n// relatively. It is NOT exported from the package.\nimport { MapAnchor } from './MapAnchor.js';\n\n// Spawn a new object at runtime and anchor it to the map\nconst obj = instantiate(myPrefab);\nconst anchor = addNewComponent(obj, new MapAnchor());\nanchor.hideUntilLocalized = true;\nanchor.offset.set(0.5, 0, -1.0);\n\n// Connect it — places immediately if localization already succeeded\nthis.adapter.registerAnchor(anchor);\n```\n\n**Using `useDefaultButton: false`** (custom button):\n\n```typescript\nconst adapter = new NeedleAdapter({\n  client,\n  useDefaultButton: false,\n  sessionOptions: { autoLocalize: true },\n});\n\naddNewComponent(this.gameObject, adapter);\nawait client.authorize();\n\nmyButton.addEventListener('click', () => {\n  if (adapter.isActive()) {\n    adapter.stopSession();\n  } else {\n    void adapter.startSession();\n  }\n});\n```\n\n---\n\n## Navigation\n\nAR indoor wayfinding on top of localization: pick a destination, follow an arrowed path along the\nfloor, arrive. Ships as a separate entry point, so an app that only localizes pays nothing for it.\n\n```bash\nnpm install @multisetai/vps three-pathfinding\n```\n\n`three-pathfinding` is an **optional** peer dependency, loaded on demand — install it only if you\nuse `NavMeshPathfinder`.\n\n**The package has the essentials to build a navigation app; it is not a navigation app.** You get\nthe state machine, pathfinding, and `buildPathRibbon` (corners → ribbon triangles). UI, shaders,\nmaterials, labels and icons are design decisions and live in the samples, where you own them.\n\n```ts\nimport { Navigation, NavMeshPathfinder, buildPathRibbon } from '@multisetai/vps/navigation';\n\nconst pathfinder = await NavMeshPathfinder.fromObject3D(navMesh, { space: mapSpace.object });\nconst navigation = await Navigation.create({ adapter, mapSpace, pathfinder, pois });\n\nnavigation.on('pathUpdated', ({ corners }) => {\n  mesh.geometry = buildPathRibbon(corners);   // your Mesh, your material\n});\nnavigation.setDestination('kitchen');\n```\n\nWorks with `ThreeAdapter` and `NeedleAdapter` alike — `Navigation` talks to\n[`IVpsAdapter`](#mapspace) and never imports either. It requires a [`MapSpace`](#mapspace), which\ndefines the coordinate frame every route is computed in; that is what makes relocalization free.\n\nFor a complete app — arrow shader, POI labels, destination panel, scene setup — start from the\n**Needle** or **three.js** navigation sample.\n\n→ **[Full navigation guide](https://docs.multiset.ai/multiset/webxr-sdk/navigation)** — setup, API, tuning, and troubleshooting.\n\n---\n\n## Canvas Visibility During AR\n\n> **Important** — the SDK renders the Three.js scene into the **XR framebuffer**, not the canvas element. During an active AR session the canvas element is not updated; it retains whatever was last drawn by the preview loop. If the canvas is visible during AR (e.g. as part of the WebXR DOM overlay) it will appear as a frozen image on top of the AR scene.\n\nAlways hide the canvas when the session starts and restore it when it ends:\n\n```typescript\nonSessionStart: () => { renderer.domElement.style.display = 'none'; },\nonSessionEnd:   () => { renderer.domElement.style.display = 'block'; },\n```\n\n---\n\n## API Reference\n\n### `MultisetClient`\n\nPure HTTP client for auth, localization, and object tracking. No WebXR or rendering concerns.\n\n```typescript\nnew MultisetClient(config: IMultisetClientConfig)\n```\n\n#### `IMultisetClientConfig`\n\n**VPS mode (`mapType: 'map'` or `mapType: 'map-set'`)**\n\n| Parameter | Type | Description |\n|---|---|---|\n| `clientId` | `string` | Your MultiSet client ID |\n| `clientSecret` | `string` | Your MultiSet client secret |\n| `mapType` | `'map' \\| 'map-set'` | Whether `code` is a single map or a map set |\n| `code` | `string` | Map or map-set code |\n| `endpoints?` | `Partial<IMultisetSdkEndpoints>` | Override default API endpoints |\n| `isRightHanded?` | `boolean` | Handedness sent to the API. Default `true` |\n| `convertToGeoCoordinates?` | `boolean` | Request geographic coordinates in the response |\n| `hintPosition?` | `string` | Local-space position hint `\"x,y,z\"` |\n| `hintRadius?` | `number \\| string` | Search radius in metres (1–100). Requires `hintPosition` or `passGeoPose` |\n| `hintMapCodes?` | `string[]` | Narrow candidates by map code. Only valid when `mapType: 'map-set'` |\n| `passGeoPose?` | `boolean` | Send a `geoHint` from the Geolocation API with each request |\n| `use2DFiltering?` | `boolean` | Skip altitude in geo filtering. Only valid when `passGeoPose: true` |\n\n**Object tracking mode (`mapType: 'object-tracking'`)**\n\n| Parameter | Type | Description |\n|---|---|---|\n| `clientId` | `string` | Your MultiSet client ID |\n| `clientSecret` | `string` | Your MultiSet client secret |\n| `mapType` | `'object-tracking'` | Enables object tracking mode. No map code required. |\n| `code` | `string[]` | Object codes to detect and track |\n| `isRightHanded?` | `boolean` | Handedness sent to the API. Default `true` |\n| `endpoints?` | `Partial<IMultisetSdkEndpoints>` | Override default API endpoints |\n\n#### Methods\n\n| Method | Returns | Description |\n|---|---|---|\n| `authorize()` | `Promise<string>` | Authenticate and cache an access token. Call before any other method. |\n| `localizeWithFrame(frame, intrinsics)` | `Promise<ILocalizeAndMapDetails \\| null>` | Submit a captured frame for VPS localization. |\n| `trackObject(frame, intrinsics)` | `Promise<IObjectTrackingResponse \\| null>` | Submit a captured frame for object detection. Uses `code` from the client config. Returns `null` when no object is detected. |\n| `downloadObjectMesh(objectCode)` | `Promise<string \\| null>` | Fetch a signed download URL for the 3D mesh of an object. |\n| `fetchMapDetails(mapCode)` | `Promise<IGetMapsDetailsResponse \\| null>` | Fetch map metadata by code (result is cached). |\n| `downloadFile(key)` | `Promise<string>` | Resolve a storage key to a signed download URL. Low-level — `downloadObjectMesh` and `fetchMapDetails` use it internally. |\n| `token` | `string \\| null` | *Getter.* The cached access token, or `null` before `authorize()`. Useful when debugging auth failures. |\n| `mapType` | `'map' \\| 'map-set' \\| 'object-tracking'` | *Getter.* The configured mode. |\n| `objectCodes` | `string[]` | *Getter.* The configured object codes. Empty unless `mapType: 'object-tracking'`. |\n\n---\n\n### `XRSessionManager`\n\nOwns the WebXR session lifecycle — frame loop, camera capture, localization, object tracking, and tracking-loss recovery. Zero Three.js dependency.\n\n```typescript\nimport { XRSessionManager } from '@multisetai/vps/core';\n\nnew XRSessionManager(gl: WebGL2RenderingContext, options: IXRSessionOptions)\n```\n\n#### `IXRSessionOptions`\n\n| Parameter | Type | Description |\n|---|---|---|\n| `client` | `MultisetClient` | Required. |\n| `overlayRoot?` | `HTMLElement` | Root element for the WebXR DOM overlay. |\n| `autoLocalize?` | `boolean` | Run one localization automatically when the session starts. |\n| `relocalization?` | `boolean` | Re-localize whenever tracking is lost and then recovered. |\n| `confidenceCheck?` | `boolean` | Only accept results with `confidence >= confidenceThreshold`. Applies to both VPS and object tracking. |\n| `confidenceThreshold?` | `number` | Minimum confidence (0.2–0.8). Default `0.5`. |\n| `poseTimeoutMs?` | `number` | Max ms to wait for a valid viewer pose before failing. Default `10000`. |\n| `localizationTrackingTimeoutMs?` | `number` | **Deprecated** — use `poseTimeoutMs`. Will be removed in v3. |\n| `backgroundLocalization?` | `boolean` | Periodically send localization/tracking requests in the background while the session is active. |\n| `bgLocalizationInterval?` | `number` | Interval in seconds between background attempts. Clamped to 10–180 s. Default `30` for VPS modes, `10` for object tracking. |\n| `autoTracking?` | `boolean` | Call `trackObjects()` once automatically when the session starts. Requires `mapType: 'object-tracking'` on the client. |\n| `restartTracking?` | `boolean` | Re-attempt tracking whenever XR tracking is lost and then recovered. |\n| `trackingCaptureDelayMs?` | `number` | Milliseconds to wait before capturing a frame when `trackObjects()` is called. Useful for camera stabilisation. Default `0`. |\n| `referenceSpaceType?` | `XRReferenceSpaceType` | XR reference space type. Default `'local'`. Use `'local-floor'` for floor-relative tracking if the device supports it. |\n| `framebufferScaleFactor?` | `number` | XR framebuffer scale relative to native resolution. Values `< 1` reduce GPU load; values `> 1` supersample. |\n| `onSessionStart?` | `() => void` | Called when the AR session starts. |\n| `onSessionEnd?` | `() => void` | Called when the AR session ends. |\n| `onLocalizationInit?` | `() => void` | Called at the start of a VPS localization run. |\n| `onLocalizationResult?` | `(result: ILocalizeAndMapDetails) => void` | Called when VPS localization succeeds (and passes the confidence check, if enabled). |\n| `onLocalizationSuccess?` | `(result: ILocalizeAndMapDetails) => void` | **Deprecated** — use `onLocalizationResult`. If using `ThreeAdapter`, use its `onLocalizationSuccess` which also provides `worldFromMap`. Will be removed in v3. |\n| `onLocalizationFailure?` | `(reason?: string) => void` | Called when VPS localization fails or falls below the confidence threshold. |\n| `onFrameCaptured?` | `(frame: IFrameCaptureEvent) => void` | Called when a camera frame is captured for localization. |\n| `onCameraIntrinsics?` | `(intrinsics: ICameraIntrinsicsEvent) => void` | Called with camera intrinsic parameters for the captured frame. |\n| `onPoseResult?` | `(pose: IPoseResultEvent) => void` | Called with the raw pose result from the VPS backend. |\n| `onObjectTrackingInit?` | `() => void` | Called at the start of an object tracking run. |\n| `onObjectTrackingRequested?` | `(frame: IFrameCaptureEvent, intrinsics: ICameraIntrinsicsEvent) => void` | Called just before the tracking request is sent, with the captured frame and intrinsics. |\n| `onObjectTrackingSuccess?` | `(result: IObjectTrackingResponse) => void` | Called when object tracking succeeds and passes the confidence check (if enabled). |\n| `onObjectTrackingFailure?` | `(reason?: string) => void` | Called when object tracking fails or falls below the confidence threshold. |\n| `onError?` | `(error: unknown) => void` | Called when any error occurs. |\n| `onContextLost?` | `() => void` | Called when the WebGL context is lost. The active session is ended automatically. |\n| `onContextRestored?` | `() => void` | Called when the WebGL context is restored. The user may restart the session. |\n\n#### Static methods\n\n| Method | Returns | Description |\n|---|---|---|\n| `XRSessionManager.isSupported()` | `Promise<boolean>` | Returns `true` if the browser supports `immersive-ar` WebXR sessions. Use this to conditionally show AR UI before creating any objects. |\n\n#### Methods\n\n| Method | Returns | Description |\n|---|---|---|\n| `createButton()` | `HTMLButtonElement` | Create the built-in styled AR button. Shows **START AR** / **STOP AR** and toggles the session on click. |\n| `startSession()` | `Promise<void>` | Start an AR session programmatically. **Must be called from within a user gesture handler** (click/tap). |\n| `stopSession()` | `void` | Stop the active AR session. No-op if no session is running. |\n| `localizeFrame()` | `Promise<ILocalizeAndMapDetails \\| null>` | Capture and localize one frame against the configured map. Requires an active session. |\n| `trackObjects()` | `Promise<IObjectTrackingResponse \\| null>` | Capture one frame and run object detection. Requires an active session and `mapType: 'object-tracking'` on the client. |\n| `isActive()` | `boolean` | Whether an XR session is currently running. |\n| `isLocalizing` | `boolean` | Whether a localization or tracking run is currently in progress. |\n| `getClient()` | `MultisetClient` | Access the underlying `MultisetClient`. |\n| `getBaseLayer()` | `XRWebGLLayer \\| null` | The session's base layer, or `null` outside a session. Only needed if you render yourself — the adapters use it to size their XR framebuffer. |\n| `getXRSession()` | `XRSession \\| null` | The live WebXR session, or `null` when none is running. Mainly useful for `domOverlayState`: `dom-overlay` is only an *optional* feature of the session request, so without it HTML mounted over the session still renders but never receives taps — and nothing else will tell you why. |\n| `getOverlayRoot()` | `HTMLElement \\| undefined` | The element passed as `overlayRoot`. Mount in-session UI here so it receives taps while AR is presenting. |\n| `dispose()` | `void` | End the session, clear background timers, remove context loss listeners, and release all resources. |\n\n#### Adapter hooks\n\nUsed internally by `ThreeAdapter`. Only call these when building a custom renderer adapter.\n\n| Method | Description |\n|---|---|\n| `setXRFrameHandler(fn)` | Called every XR frame with pose, view, viewport, and framebuffer info. |\n| `setAdapterResultHandler(fn)` | Called after a successful VPS localization with the result and tracker-space matrix. |\n| `setAdapterObjectTrackingHandler(fn)` | Called after a successful object tracking result with the result and tracker-space matrix. |\n| `setAdapterSessionHandlers(onStart, onEnd)` | Called on session start/end, before user callbacks. |\n\n---\n\n### `ThreeAdapter`\n\nWires `XRSessionManager` to a Three.js renderer. Handles XR framebuffer binding, camera matrix sync, preview loop, resize, and optional map mesh / gizmo / object mesh display.\n\n```typescript\nimport { ThreeAdapter } from '@multisetai/vps/three';\n\nnew ThreeAdapter(options: IThreeAdapterOptions)\n```\n\n#### `IThreeAdapterOptions`\n\n| Parameter | Type | Description |\n|---|---|---|\n| `session` | `XRSessionManager` | Required. |\n| `renderer` | `THREE.WebGLRenderer` | Required. |\n| `scene` | `THREE.Scene` | Required. |\n| `camera` | `THREE.PerspectiveCamera` | Required. |\n| `showMesh?` | `boolean` | Show the VPS map mesh after localization. Default `false`. |\n| `showGizmo?` | `boolean` | Show a transform gizmo after localization. Default `true`. |\n| `showObjectMeshes?` | `boolean` | Load and display a 3D outline mesh for each detected object. Default `false`. |\n| `useDefaultButton?` | `boolean` | Mount the built-in START AR / STOP AR button. Default `true`. Set to `false` to drive the session via `startSession()` / `stopSession()`. |\n| `buttonContainer?` | `HTMLElement` | Where to append the built-in button. Defaults to `overlayRoot` or `document.body`. |\n| `onButtonCreated?` | `(button: HTMLButtonElement) => void` | Called after the built-in button is created. |\n| `onXRFrame?` | `(event: IXRFrameEvent) => void` | Called every XR frame after camera matrices are synced, before the scene is rendered. Use this to update scene objects each frame. |\n| `onLocalizationSuccess?` | `(result: ILocalizeAndMapDetails, worldFromMap: THREE.Matrix4) => void` | Called immediately after a successful VPS localization. `worldFromMap` transforms map-space coordinates to Three.js world space — use it to place content at known map coordinates. |\n| `onObjectMeshLoaded?` | `(objectCode: string) => void` | Called when a detected object's 3D mesh has been loaded and placed in the scene. Only fires when `showObjectMeshes: true`. |\n\n#### Static methods\n\n| Method | Returns | Description |\n|---|---|---|\n| `ThreeAdapter.isSupported()` | `Promise<boolean>` | Returns `true` if the browser supports `immersive-ar` WebXR sessions. |\n\n#### Methods\n\n| Method | Returns | Description |\n|---|---|---|\n| `initialize(buttonContainer?)` | `HTMLButtonElement \\| null` | Start the preview render loop, attach resize handler, and mount the built-in button. Returns `null` when `useDefaultButton: false`. |\n| `isActive()` | `boolean` | Whether an XR session is currently running. |\n| `isLocalizing` | `boolean` | Whether a localization or tracking run is currently in progress. |\n| `startSession()` | `Promise<void>` | Start an AR session. **Must be called from within a user gesture handler.** |\n| `stopSession()` | `void` | Stop the active AR session. No-op if no session is running. |\n| `localizeFrame()` | `Promise<ILocalizeAndMapDetails \\| null>` | Capture and localize one frame. |\n| `trackObjects()` | `Promise<IObjectTrackingResponse \\| null>` | Capture one frame and run object detection. Requires `mapType: 'object-tracking'` on the client. |\n| `clearObjectMeshes()` | `void` | Remove all object meshes from the scene that were placed by `showObjectMeshes`. |\n| `dispose()` | `void` | Stop loops, remove listeners, dispose Three.js resources, and end the session. |\n\n#### Events and scene access\n\nThe `onLocalizationSuccess` / `onXRFrame` options are single-slot: setting one replaces it. Use these\nlisteners instead when more than one part of your app needs to react — anything layered on top of\nthe adapter (`MapSpace`, `Navigation`, your own code) uses them, so they compose.\n\nEvery `add*Listener` **returns its own unsubscribe function**, which is usually easier than keeping\na reference for the matching `remove*Listener`.\n\n| Method | Returns | Description |\n|---|---|---|\n| `addLocalizationListener(fn)` | `() => void` | `fn(result, worldFromMap)` after every successful localization. |\n| `removeLocalizationListener(fn)` | `void` | Unsubscribe. Equivalent to calling the returned function. |\n| `addSessionStartListener(fn)` | `() => void` | Fires when the AR session starts. |\n| `removeSessionStartListener(fn)` | `void` | Unsubscribe. |\n| `addSessionEndListener(fn)` | `() => void` | Fires when the AR session ends. |\n| `removeSessionEndListener(fn)` | `void` | Unsubscribe. |\n| `addFrameListener(fn)` | `() => void` | `fn(event)` every XR frame, after camera matrices are synced and before the scene renders — so anything you move lands in the same frame. |\n| `removeFrameListener(fn)` | `void` | Unsubscribe. |\n| `waitForLocalization()` | `Promise<ILocalizeAndMapDetails>` | Resolves immediately if this session has already localized, otherwise on the next success. Never rejects. Replaces the usual \"do X once we are localized\" callback dance. |\n| `getLastLocalization()` | `ILocalizationSnapshot \\| null` | The current session's most recent result plus its `worldFromMap`. Cleared on session end, so a stale pose can never be replayed into a new session. |\n| `getScene()` | `THREE.Scene` | The scene passed in options. |\n| `getCamera()` | `THREE.Camera` | The XR-driven camera. **Read its pose with `getWorldPosition()` / `matrixWorld`, never `camera.position`** — see the warning under [Placing Content](#placing-content-at-map-coordinates). |\n\n---\n\n### `NeedleAdapter`\n\nNeedle Engine `Behaviour` that wires `XRSessionManager` to Needle's renderer and scene. Handles XR framebuffer binding, camera matrix sync, AR passthrough (transparent background), and optional map mesh / gizmo / object mesh display.\n\n```typescript\nimport { NeedleAdapter } from '@multisetai/vps/needle';\n\nnew NeedleAdapter(options: INeedleAdapterOptions)\n```\n\nAdd to a scene via `addNewComponent(gameObject, adapter)` — this triggers `awake()`, which creates the session and mounts the button.\n\n#### `INeedleAdapterOptions`\n\n| Parameter | Type | Description |\n|---|---|---|\n| `client` | `MultisetClient` | Required. |\n| `sessionOptions?` | `Omit<IXRSessionOptions, 'client'>` | All session options and callbacks — forwarded directly to `XRSessionManager`. See [IXRSessionOptions](#ixrsessionoptions) for the full list. |\n| `showMesh?` | `boolean` | Show the VPS map mesh after localization. Default `false`. |\n| `showGizmo?` | `boolean` | Show a transform gizmo after localization. Default `false`. |\n| `showObjectMeshes?` | `boolean` | Load and display a 3D outline mesh for each detected object. Default `false`. |\n| `useDefaultButton?` | `boolean` | Mount the built-in START AR / STOP AR button automatically in `awake()`. Default `true`. Set to `false` to drive the session via `startSession()` / `stopSession()`. |\n| `buttonContainer?` | `HTMLElement` | Where to append the built-in button. Defaults to `overlayRoot` or `document.body`. |\n| `onButtonCreated?` | `(button: HTMLButtonElement) => void` | Called after the built-in button is created. |\n| `onXRFrame?` | `(event: IXRFrameEvent) => void` | Called every XR frame after camera matrices are synced, before the scene is rendered. |\n| `onLocalizationSuccess?` | `(result: ILocalizeAndMapDetails, worldFromMap: THREE.Matrix4) => void` | Called after a successful VPS localization. `worldFromMap` transforms map-space coordinates to Three.js world space. |\n| `onObjectMeshLoaded?` | `(objectCode: string) => void` | Called when a detected object's 3D mesh has been loaded and placed in the scene. Only fires when `showObjectMeshes: true`. |\n\n#### Static methods\n\n| Method | Returns | Description |\n|---|---|---|\n| `NeedleAdapter.isSupported()` | `Promise<boolean>` | Returns `true` if the browser supports `immersive-ar` WebXR sessions. |\n\n#### Methods\n\n| Method | Returns | Description |\n|---|---|---|\n| `isActive()` | `boolean` | Whether an XR session is currently running. |\n| `isLocalizing` | `boolean` | Whether a localization or tracking run is currently in progress. |\n| `startSession()` | `Promise<void>` | Start an AR session. **Must be called from within a user gesture handler.** |\n| `stopSession()` | `void` | Stop the active AR session. No-op if no session is running. |\n| `localizeFrame()` | `Promise<ILocalizeAndMapDetails \\| null>` | Capture and localize one frame. |\n| `trackObjects()` | `Promise<IObjectTrackingResponse \\| null>` | Capture one frame and run object detection. Requires `mapType: 'object-tracking'` on the client. |\n| `clearObjectMeshes()` | `void` | Remove all object meshes placed by `showObjectMeshes`. |\n| `registerAnchor(anchor)` | `void` | Connect a dynamically created `MapAnchor` to the adapter. Registers localization listeners and immediately applies the last localization result if the session is already active. |\n| `getSession()` | `XRSessionManager` | Access the underlying session manager directly. Use only when you need low-level session control — for example `getSession().getOverlayRoot()` to mount UI that receives taps during AR. |\n\n#### Events and scene access\n\nIdentical to [`ThreeAdapter`](#threeadapter) — both adapters satisfy the same\n[`IVpsAdapter`](#ivpsadapter) contract, which is what lets features like `MapSpace` and\n`Navigation` work on either without change.\n\nEvery `add*Listener` **returns its own unsubscribe function**. Either call that or the matching\n`remove*Listener` from your component's `onDestroy` — skipping it leaves the adapter holding a\nreference to a destroyed component.\n\n| Method | Returns | Description |\n|---|---|---|\n| `addLocalizationListener(fn)` | `() => void` | `fn(result, worldFromMap)` after every successful localization. Prefer this over the constructor's `onLocalizationSuccess` when several components need to react. |\n| `removeLocalizationListener(fn)` | `void` | Unsubscribe. |\n| `addSessionStartListener(fn)` | `() => void` | Fires when AR begins. Useful for showing in-session UI. |\n| `removeSessionStartListener(fn)` | `void` | Unsubscribe. |\n| `addSessionEndListener(fn)` | `() => void` | Fires when AR ends. Useful for hiding anchored content between sessions. |\n| `removeSessionEndListener(fn)` | `void` | Unsubscribe. |\n| `addFrameListener(fn)` | `() => void` | `fn(event)` every XR frame, after camera matrices are synced and before the scene renders. |\n| `removeFrameListener(fn)` | `void` | Unsubscribe. |\n| `waitForLocalization()` | `Promise<ILocalizeAndMapDetails>` | Resolves immediately if this session has already localized, otherwise on the next success. Never rejects. |\n| `getLastLocalization()` | `ILocalizationSnapshot \\| null` | The current session's most recent result plus its `worldFromMap`. Cleared on session end. |\n| `getScene()` | `THREE.Scene` | Needle's scene. |\n| `getCamera()` | `THREE.Camera` | The XR-driven camera. **Read its pose with `getWorldPosition()` / `matrixWorld`, never `camera.position`.** |\n\n---\n\n### `IVpsAdapter`\n\nThe surface shared by `ThreeAdapter` and `NeedleAdapter`. Type against this and your code works on\neither — and on any future adapter.\n\n```typescript\nimport type { IVpsAdapter } from '@multisetai/vps/three';\n\nfunction attachMyFeature(adapter: IVpsAdapter) {\n  const off = adapter.addLocalizationListener((result, worldFromMap) => { /* ... */ });\n  return off;\n}\n```\n\nBoth adapters satisfy it **structurally** — neither declares `implements IVpsAdapter` — and a\ncompile-time guard in each entry point fails the build if one drifts from the interface.\n\nIt covers: `isActive()`, `isLocalizing`, `getScene()`, `getCamera()`, `getLastLocalization()`, the\nfour `add*Listener` / `remove*Listener` pairs, `waitForLocalization()`, `startSession()`,\n`stopSession()`, `localizeFrame()` and `trackObjects()`. This is what `MapSpace` and `Navigation`\ndepend on, which is why neither imports a concrete adapter.\n\n`ILocalizationSnapshot` is `{ result: ILocalizeAndMapDetails; worldFromMap: THREE.Matrix4 }`.\n\n---\n\n### `MapSpace`\n\nThe VPS map coordinate frame. Nest your map-anchored content under it, and on every successful\nlocalization it is moved so its origin coincides with the scanned map's origin — every descendant\nfollows by ordinary parenting.\n\n**A child's local position is therefore its map coordinate.** That is what makes it cheap:\nrelocalization, background localization and session restarts change only this one transform, so\nnothing computed relative to it ever needs recomputing.\n\nIt exists in two forms, and they are the same object:\n\n| | Import | Use in |\n|---|---|---|\n| **Package class** | `import { MapSpace } from '@multisetai/vps/three'` | plain three.js, or from code in a Needle project |\n| **Needle component** | `import { MapSpace } from './MapSpace.js'` | Unity — a [template file](#unity-inspector-workflow) you copy into `src/scripts/` |\n\nThe template is a thin `Behaviour` that owns a package `MapSpace` bound to its GameObject and\nforwards to it, so behaviour is identical. The Unity→three.js handedness correction lives in the\npackage class — one verified implementation rather than a copy in every project.\n\n```typescript\nimport { MapSpace } from '@multisetai/vps/three';\n\nconst mapSpace = new MapSpace(new THREE.Object3D());\nscene.add(mapSpace.object);\nmapSpace.connect(adapter);                                  // ThreeAdapter or NeedleAdapter\n\nmapSpace.add(marker, new THREE.Vector3(1.5, 0, -2));        // position IS a map coordinate\n```\n\n#### Options and fields\n\n| Name | Type | Default | Description |\n|---|---|---|---|\n| `hideUntilLocalized` | `boolean` | `true` | Hide the object until the first localization, and re-hide on session end. Read live, so you can set it to `false` at runtime to keep content visible between sessions. **In Needle this deactivates the entire subtree** — see the [setup notes](#mapspace--anchoring-a-whole-subtree-unity). |\n\n#### Methods\n\n| Method | Returns | Description |\n|---|---|---|\n| `connect(adapter)` | `void` | Subscribe to an adapter so this frame is placed on every localization. Safe to call repeatedly — previous subscriptions are removed first. If the adapter has already localized in the current session, that result is replayed immediately, so a `MapSpace` created mid-session is placed at once. |\n| `disconnect()` | `void` | Remove every subscription made by `connect()`. |\n| `applyLocalization(result, worldFromMap)` | `void` | Place the frame directly, bypassing events. Useful in tests, or from a component that receives the result by its own route. |\n| `add(object, mapCoordinate?)` | `void` | Parent an object to this frame, optionally at a map coordinate. |\n| `mapToWorld(v, target?)` | `THREE.Vector3` | Map space → world space. |\n| `worldToMap(v, target?)` | `THREE.Vector3` | World space → map space. Use this to convert the camera pose before any map-space calculation. |\n| `dispose()` | `void` | Same as `disconnect()`. |\n| `object` | `THREE.Object3D` | *Getter.* The wrapped object — add it to your scene. |\n| `isLocalized` | `boolean` | *Getter.* True once a localization has been applied in the current session. |\n| `MapSpace.toLocal(unityCoord, target?)` | `THREE.Vector3` | **Static.** Convert a Unity/left-handed coordinate copied from the portal's Map Viewer into map space. Editor-authored children get this from Needle's exporter and must **not** use it; content created in code must. |\n\n#### In Unity\n\n`MultisetVPS` discovers and wires all `MapSpace` components at startup. A scene should contain\nexactly one — `MultisetVPS` warns if it finds more, since they would all be moved to the same\norigin. The Needle component also exposes `connectAdapter(adapter)` and\n`applyLocalization(result, worldFromMap)`, matching `MapAnchor`, so it can be registered at runtime\nwith `NeedleAdapter.registerAnchor(mapSpace)`. Reach the underlying package object through\n`.space` — for example `mapSpace.space.worldToMap(...)`.\n\n---\n\n### `MapAnchor`\n\nNeedle Engine `Behaviour` that anchors a single GameObject to the VPS map origin after localization.\n\n> **Not exported from the package** — `MapAnchor` is a [template file](#unity-inspector-workflow) you copy into your own `src/scripts/` folder. Import it relatively: `import { MapAnchor } from './MapAnchor.js'`.\n\nIn Unity, add the component via the Inspector — `MultisetVPS` discovers and wires all `MapAnchor` instances at startup automatically. For laying out multiple POIs, prefer [`MapSpace`](#mapspace). Do not nest a `MapAnchor` inside a `MapSpace` — the offset would be applied twice.\n\nFor runtime-spawned objects use `NeedleAdapter.registerAnchor()` (see [MapAnchor — zero-code object placement](#mapanchor--zero-code-object-placement-unity) above).\n\n#### Fields\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `offset` | `THREE.Vector3` | `(0, 0, 0)` | Position offset from the map origin in metres. When `isRightHanded` is `false` (default), enter Unity Inspector values directly — X is negated automatically. When `true`, supply Three.js values as-is. |\n| `matchOrientation` | `boolean` | `true` | Align the object's rotation to the map's orientation. |\n| `rotationOffset` | `THREE.Euler` | `(0°, 0°, 0°)` | Additional rotation applied on top of the map orientation. Only applied when `matchOrientation` is `true`. When `isRightHanded` is `false` (default), enter Unity Inspector values — Y and Z are negated automatically. |\n| `hideUntilLocalized` | `boolean` | `true` | Hide the object until the first successful localization. Re-hides on session end. |\n| `isRightHanded` | `boolean` | `false` | When `false` (default), `offset` and `rotationOffset` are treated as Unity (left-handed) values and converted automatically (negate X in position; negate Y and Z in rotation). Set to `true` if you are supplying Three.js (right-handed) values directly. |\n\n#### Methods\n\n| Method | Description |\n|---|---|\n| `connectAdapter(adapter)` | Called automatically by `MultisetVPS`. Registers localization and session-end listeners on the adapter. |\n| `applyLocalization(result, worldFromMap)` | Apply a localization result directly — positions and shows the object. Called internally by `registerAnchor`; also useful for testing or custom placement logic. |\n\n---\n\n### `Navigation`\n\nAR wayfinding state machine. Adapter-agnostic and free of DOM, geometry and renderer assumptions —\nsee the [full navigation guide](https://docs.multiset.ai/multiset/webxr-sdk/navigation).\n\n```typescript\nimport { Navigation, NavMeshPathfinder, buildPathRibbon } from '@multisetai/vps/navigation';\n\nconst pathfinder = await NavMeshPathfinder.fromObject3D(navMesh, { space: mapSpace.object });\nconst navigation = await Navigation.create({ adapter, mapSpace, pathfinder, pois });\n```\n\nRequires a [`MapSpace`](#mapspace) — every route is computed in map space, which is what makes\nrelocalization free.\n\n#### Methods and properties\n\n| Member | Returns | Description |\n|---|---|---|\n| `Navigation.create(options)` | `Promise<Navigation>` | **Static.** Build and attach. Async so a future wasm-backed pathfinder can be awaited without changing call sites. |\n| `setDestination(target)` | `void` | Accepts an `IMapPOI`, a registered POI id, or a bare map coordinate. Emits `unreachable` and refuses to start if no route exists. |\n| `stop()` | `void` | Stop navigating and clear the path. |\n| `recalculate()` | `void` | Force an immediate recalculation, ignoring the interval and movement threshold. |\n| `state` | `NavigationState` | `'unlocalized'` \\| `'idle'` \\| `'navigating'` \\| `'off-navmesh'` \\| `'arrived'`. |\n| `destination` | `IMapPOI \\| null` | |\n| `currentPath` | `readonly THREE.Vector3[]` | Corners, in map space. Empty when not navigating. |\n| `remainingDistance` | `number` | Metres left along the current path. |\n| `pois` | `readonly IMapPOI[]` | |\n| `setPOIs(list)` / `addPOI(poi)` / `removePOI(id)` / `getPOI(id)` | | Manage the POI registry at runtime. Removing the active destination stops navigation. |\n| `distanceTo(poi)` | `number` | Walking distance in metres, or `-1` when unknown. Throttled and cached, so it is safe to call per frame. |\n| `isReachable(poi)` | `boolean` | |\n| `nearestPOI()` | `IMapPOI \\| null` | Closest by walking distance, skipping unreachable ones. |\n| `getViewerMapPosition(target?)` | `THREE.Vector3 \\| null` | Viewer position in map space, or `null` before the first localization. |\n| `diagnose()` | `NavigationDiagnosis` | `'ok'` \\| `'no-navmesh'` \\| `'not-localized'` \\| `'off-navmesh'` \\| `'no-pois'` \\| `'pois-off-navmesh'`. **The first thing to call when navigation \"does nothing\".** |\n| `on(event, fn)` | `() => void` | Subscribe; returns unsubscribe. Events below. |\n| `attach()` / `detach()` | `void` | Subscribe to / unsubscribe from the adapter. `create()` attaches for you. |\n| `update(deltaSeconds)` | `void` | Advance manually. Only needed if you drive your own loop — normally the adapter ticks it. |\n| `dispose()` | `void` | |\n| `Navigation.pathLength(corners)` | `number` | **Static.** Summed distance between consecutive corners. |\n\n#### Events\n\n| Event | Payload |\n|---|---|\n| `stateChanged` | `{ state, previous }` |\n| `destinationChanged` | `IMapPOI \\| null` |\n| `pathUpdated` | `{ corners, remainingDistance }` |\n| `arrived` | `IMapPOI` |\n| `unreachable` | `IMapPOI` — no complete route; navigation did not start |\n| `tick` | `{ deltaSeconds }` — every frame, from the XR loop in-session and `requestAnimationFrame` outside one, so animation and UI work in a desktop preview |\n\n---\n\n### `NavMeshPathfinder`\n\n`three-pathfinding` behind a contract that holds. `three-pathfinding` is an **optional** peer\ndependency, loaded on demand — install it only if you build one of these.\n\n| Member | Returns | Description |\n|---|---|---|\n| `NavMeshPathfinder.fromObject3D(object, options?)` | `Promise<NavMeshPathfinder>` | **Static.** Merges every descendant mesh and transforms it into `options.space` — pass `mapSpace.object`. |\n| `NavMeshPathfinder.fromGeometry(geometry, options?)` | `Promise<NavMeshPathfinder>` | **Static.** From geometry already in map space. |\n| `findPath(from, to)` | `THREE.Vector3[] \\| null` | Corners inclusive of both ends. `null` means no *complete* route — a partial path is never returned as success. |\n| `clampToNavMesh(p, maxDistance?)` | `THREE.Vector3 \\| null` | The **viewer's** projection: 3D distance, preferring the surface beneath. |\n| `snapDestination(p, label?)` | `THREE.Vector3 \\| null` | A **destination's** projection: horizontal distance only, so any height works. |\n| `geometry` | `THREE.BufferGeometry` | *Getter.* The merged navmesh, in map space. Use it to build a debug overlay. |\n| `groupCount` | `number` | *Getter.* Disconnected walkable regions. More than one on a single floor means the navmesh is torn. |\n| `dispose()` | `void` | |\n\nOptions: `space`, `weldTolerance`, `destinationSnapRadius` (default 1 m, horizontal),\n`destinationSnapWarnHeight` (3 m), `startRegionTolerance` (1 m). The\n[navigation guide](https://docs.multiset.ai/multiset/webxr-sdk/navigation#tuning) explains why destination height is ignored and why\nthese defaults are what they are.\n\n`StraightLinePathfinder` implements the same `IPathfinder` interface and walks straight through\nwalls — for development before a navmesh exists.\n\n### `buildPathRibbon(corners, options?)`\n\nPath corners → ribbon triangles. The only rendering code in the package, because it is the only part\nwith no design content and one correct answer. You own the `Mesh` and the material:\n\n```typescript\nmesh.geometry = buildPathRibbon(corners, { width: 0.35, heightAboveFloor: 0.1 });\n```\n\nVertex contract, stable API — write a shader against it: **`uv.x` = distance along the path in\nmetres** (cumulative, horizontal, *not* normalised), **`uv.y` = 0…1 across the width**, two vertices\nper corner, one quad per segment, indexed, no normals. Metres keep a pattern's real-world size\nconstant whatever the path length, and let it flow unbroken across corners.\n\n#### Options\n\n| Option | Default | Description |\n|---|---|---|\n| `width` | `0.35` | Ribbon width in metres. |\n| `heightAboveFloor` | `0.1` | Lift above the walkable surface. Raise it if the ribbon z-fights the floor. |\n| `cornerRadius` | `0.4` | Radius used to round off interior corners. `0` gives sharp corners. |\n| `cornerSegments` | `4` | Points per rounded corner. Higher is smoother and costs triangles. |\n| `miterLimit` | `3` | Cap on how far a sharp joint may extend, as a multiple of half-width. |\n\nIt handles the cases that are easy to get wrong, each of which shipped as a visible defect before\ntesting caught it:\n\n- **Corners are rounded.** At a sharp corner the two vertices are offset along the miter — the angle\n  bisector — not perpendicular to either segment, so the quad is a trapezoid and any pattern on it is\n  sheared by up to half the turn angle: ~45° on a right angle, which reads as arrows visibly bending.\n  Rounding spreads that over a short arc, taking the worst edge angle from 45° to 76° where 90° is no\n  shear. The radius clamps per corner to 40% of the shorter adjacent segment so tight zig-zags cannot\n  fold the ribbon back on itself, and endpoints never move.\n- **Sharp miters are capped.** The 1/cos(θ/2) scale that keeps ribbon width constant through a turn\n  goes to infinity as the turn approaches 180°, firing a spike across the scene. Past the cap the\n  joint narrows instead — a pinched corner is a much better failure mode.\n- **Coincident corners are dropped.** A flat ribbon cannot express a purely vertical step, and a\n  repeated corner has no direction; both give a zero-width joint. Pathfinders do emit these, so it\n  runs on every path rather than being treated as bad input.\n- **Degenerate input returns an empty geometry** rather than NaNs, so callers can hide the mesh on\n  `geometry.getAttribute('position')?.count` instead of special-casing.\n\n#### If you tile an arrow texture on it\n\n**Arrow size and arrow spacing must be separate controls.** Mapping one texture repeat across the\nwhole spacing interval — the obvious shortcut — stretches each arrow by `spacing / width`, which for a\n0.35 m ribbon at 2 m spacing is 5.7×. Map the texture over an explicit arrow *length* and leave the\nremainder of the interval empty:\n\n```glsl\nfloat along = vUv.x - uScrollOffset;                       // metres\nfloat cell  = fract(along / max(uArrowSpacing, 0.0001));\nfloat u     = cell * uArrowSpacing / max(uArrowLength, 0.0001);\nif (u > 1.0) discard;                                      // the gap between arrows\n```\n\nDefault the arrow length to the ribbon width and a square texture comes out undistorted. Arrow art is\nalso conventionally a silhouette in the **alpha channel** with flat RGB, so treat the texture as a\nmask and take the colour from a uniform — multiplying by `texel.rgb` gives black arrows whatever\ncolour you set. The samples' `NavigationVisuals.ts` implements both.\n\n→ [Full navigation guide](https://docs.multiset.ai/multiset/webxr-sdk/navigation)\n\n---\n\n### `MultisetVPS`\n\nInspector-driven Needle Engine component that bootstraps the full VPS stack — credentials, session, UI buttons, and `MapSpace` / `MapAnchor` discovery — from Unity Inspector fields alone.\n\n> **Not exported from the package.** `MultisetVPS` is a [template file](#unity-inspector-workflow) you copy into your own `src/scripts/` folder, because Needle's Unity codegen only scans your project. Import it relatively if you need it in code:\n>\n> ```typescript\n> import { MultisetVPS } from './MultisetVPS.js';\n> import { MapType } from './MapType.js';\n> ```\n\n> See [Unity Inspector workflow](#unity-inspector-workflow) for setup. The template files handle TypeStore registration automatically — no manual `register()` call needed.\n\n#### Inspector fields\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `clientId` | `string` | `\"\"` | Your Multiset client ID. |\n| `clientSecret` | `string` | `\"\"` | Your Multiset client secret. |\n| `mapCode` | `string` | `\"\"` | Map/map-set code, or comma-separated object codes for object-tracking mode. |\n| `mapType` | `MapType` | `SingleMap` | `SingleMap`, `MapSet`, or `ObjectTracking`. |\n| `showMesh` | `boolean` | `true` | Show the VPS map mesh after localization (VPS modes only). |\n| `showGizmo` | `boolean` | `true` | Show a transform gizmo after localization (VPS modes only). |\n| `showObjectMeshes` | `boolean` | `false` | Load and display a 3D outline mesh for each detected object (OT mode only). |\n| `autoLocalize` | `boolean` | `true` | Run one localization automatically when the session starts (VPS modes). |\n| `relocalization` | `boolean` | `false` | Re-localize whenever tracking is lost and then recovered. |\n| `backgroundLocalization` | `boolean` | `false` | Periodically localize in the background while the session is active. |\n| `bgLocalizationInterval` | `number` | `0` | Seconds between background localization attempts (10–180). 0 = SDK default. |\n| `confidenceCheck` | `boolean` | `false` | Only accept results above `confidenceThreshold`. |\n| `confidenceThreshold` | `number` | `0.5` | Minimum confidence (0.2–0.8). |\n| `poseTimeoutMs` | `number` | `0` | Max ms to wait for a valid viewer pose. 0 = SDK default (10 000 ms). |\n| `convertToGeoCoordinates` | `boolean` | `false` | Request geographic coordinates in the localization response. |\n| `passGeoPose` | `boolean` | `false` | Send a geolocation hint with each request. |\n| `use2DFiltering` | `boolean` | `false` | Skip altitude in geo filtering. Only valid when `passGeoPose` is enabled. |\n| `hintPosition` | `string` | `\"\"` | Local-space position hint `\"x,y,z\"`. |\n| `hintRadius` | `number` | `0` | Search radius in metres around `hintPosition`. |\n| `hintMapCodes` | `string` | `\"\"` | Comma-separated map codes to restrict search (map-set mode only). |\n| `autoTracking` | `boolean` | `false` | Detect objects automatically when the session starts (OT mode). |\n| `restartTracking` | `boolean` | `false` | Re-attempt tracking when XR tracking is lost and recovered (OT mode). |\n| `trackingCaptureDelayMs` | `number` | `0` | Ms to wait before capturing a frame for tracking. |\n\n#### Properties\n\n| Property | Type | Description |\n|---|---|---|\n| `adapter` | `NeedleAdapter \\| null` | The underlying `NeedleAdapter` after `start()` completes. Use this to call `registerAnchor()`, add listeners, or access the session directly. `null` until start resolves. |\n\n---\n\n## Placing Content at Map Coordinates\n\n> **Using Needle Engine?** You do not need any of this. Use [`MapSpace`](#mapspace--anchoring-a-whole-subtree-unity) to lay content out in the Unity Inspector, or [`MapAnchor`](#mapanchor--zero-code-object-placement-unity) for individual objects. The section below is for `ThreeAdapter` and custom renderers.\n\nAfter localization, the `onLocalizationSuccess` callback on `ThreeAdapter` provides a `worldFromMap` matrix that converts any point from VPS map space into Three.js world space. Use this to anchor scene objects to specific physical locations in the scanned map — independently of where the user started the AR session.\n\n```typescript\nconst adapter = new ThreeAdapter({\n  session,\n  renderer, scene, camera,\n  onLocalizationSuccess: (result, worldFromMap) => {\n    // mapPoint is a position you measured from the scanned map (in metres)\n    const mapPoint = new THREE.Vector3(1.5, 0, -2.0);\n\n    const marker = new THREE.Mesh(\n      new THREE.SphereGeometry(0.05),\n      new THREE.MeshBasicMaterial({ color: 0x00ff88 })\n    );\n    marker.position.copy(mapPoint.applyMatrix4(worldFromMap));\n    scene.add(marker);\n  },\n});\n```\n\n> **Note** — `worldFromMap` is recomputed on every successful localization. If you re-localize, update or re-add your objects so they stay in sync with the latest result.\n\n---\n\n## Styling the AR Button\n\nThe built-in button ships with minimal inline styles. Use these CSS classes to override appearance from your own stylesheet:\n\n| Class | When present |\n|---|---|\n| `.multiset-a","readmeFilename":"README.md"}