← Commit history
vendor/adom-step/vendor/adom-3d-viewer-babylon9/README-FORK.mdadded+50
@@ -0,0 +1,50 @@+# adom-3d-viewer-babylon9++Fork of [`adom-inc/3d-viewer`](https://github.com/adom-inc/3d-viewer) upgraded to **Babylon.js 9.5.0** with the redesigned Inspector v9 + 3d-viewer-design-skill-compliant defaults.++## What changed from upstream++- **Babylon 7 → Babylon 9.5** — `@babylonjs/core`, `@babylonjs/loaders`, `@babylonjs/inspector` all bumped to ^9.5.0. Inspector v9 ships the redesigned tree explorer, properties panel, scene-stats, mesh-property inspectors.+- **§3b background gradient** applied (top `#5a6b7e` → bottom `#2a3340`). Replaces the upstream flat near-black that the 3d-viewer-design skill explicitly calls out as debt.+- **ESM output, not IIFE** — Babylon 9's `@babylonjs/inspector` has circular imports that resolve to TDZ if collapsed into a single IIFE (regardless of minifier). ESM preserves module boundaries so they initialize cleanly. Bundle entry is `adom-3d-viewer-babylon9.esm.js`; load via `<script type="module">`.+- **Distinct global namespace** — exposes `window.Adom3DViewerBabylon9` (not `window.Adom3DViewer`) so apps can have both bundles loaded side-by-side during migration.++## Consumer pattern++```html+<!-- Required browser shim for Node-style globals that @babylonjs/inspector's+     React (Fluent UI) deps assume — they reference process.env / global. -->+<script>+  window.process = window.process || { env: { NODE_ENV: 'production' } };+  window.global = window.global || window;+</script>++<script type="module">+  await import('./viewer/adom-3d-viewer-babylon9.esm.js');+  // window.Adom3DViewerBabylon9 is now available.+  const viewer = window.Adom3DViewerBabylon9.init(host, {+    zUp: true,+    modelUrl: 'chip.glb',+    showViewCube: true,+  });+  // viewer.loadModel(url), viewer.clearScene(), viewer.frameModel(),+  // viewer.showDebugLayer() — all the same API as upstream.+</script>+```++The bundle code-splits into ~60 chunks — serve them all from the same directory as the main `.esm.js` entry. Browsers fetch chunks on demand (`./chunk-XXX.js` relative paths).++## Build++```bash+npm install+npm run build:standalone   # → dist-standalone/adom-3d-viewer-babylon9.esm.js + chunks+```++## Roadmap (skill compliance still to land)++- §4b bottom-light toggle as a default toolbar button+- §4d medium-gray translucent ground plane + ShadowGenerator+- §6c Shift+Alt+Click orbit-center recentering+- §6b.i adaptive precision after every reframe+- §8b/c mesh-local axis helpers + screen-space corner triad
vendor/adom-step/vendor/adom-3d-viewer-babylon9/VERSIONadded+1
@@ -0,0 +1 @@+0.2.0
vendor/adom-step/vendor/adom-3d-viewer-babylon9/package-lock.jsonadded+3696
@@ -0,0 +1,3696 @@+{+  "name": "adom-3d-viewer-babylon9",+  "version": "0.2.0",+  "lockfileVersion": 3,+  "requires": true,+  "packages": {+    "": {+      "name": "adom-3d-viewer-babylon9",+      "version": "0.2.0",+      "dependencies": {+        "@babylonjs/core": "^9.5.0",+        "@babylonjs/inspector": "^9.5.0",+        "@babylonjs/loaders": "^9.5.0",+        "svelte": "^4.0.0 || ^5.0.0"+      },+      "devDependencies": {+        "@sveltejs/vite-plugin-svelte": "^3.1.2",+        "svelte-check": "^3.8.6",+        "terser": "^5.46.2",+        "typescript": "^5.5.4",+        "vite": "^5.4.1"+      },+      "peerDependencies": {+        "svelte": "^4.0.0 || ^5.0.0"+      }+    },+    "node_modules/@ampproject/remapping": {+      "version": "2.3.0",+      "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",+      "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",+      "dependencies": {+        "@jridgewell/gen-mapping": "^0.3.5",+        "@jridgewell/trace-mapping": "^0.3.24"+      },+      "engines": {+        "node": ">=6.0.0"+      }+    },+    "node_modules/@babel/runtime": {+      "version": "7.29.2",+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",+      "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",+      "peer": true,+      "engines": {+        "node": ">=6.9.0"+      }+    },+    "node_modules/@babylonjs/addons": {+      "version": "9.5.0",+      "resolved": "https://registry.npmjs.org/@babylonjs/addons/-/addons-9.5.0.tgz",+      "integrity": "sha512-lnQbW+9xA2SjJJ8Sio074Zx+WJX+bLxo2+ocHs7ETPc1wMDYJWBl7aUH3X/HhhRv9YRidVAzqmCXaxaNNmPRBQ==",+      "peer": true,+      "peerDependencies": {+        "@babylonjs/core": "^9.0.0"+      }+    },+    "node_modules/@babylonjs/core": {+      "version": "9.5.0",+      "resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-9.5.0.tgz",+      "integrity": "sha512-DTqkOTueTp/xSw/VTVzrxLpBWjamVIKXbAbgqDHRR/V3Am5F5LRGscaWjWgPc5INv7HD2zTln/lyWdm47KQkgQ=="+    },+    "node_modules/@babylonjs/gui": {+      "version": "9.5.0",+      "resolved": "https://registry.npmjs.org/@babylonjs/gui/-/gui-9.5.0.tgz",+      "integrity": "sha512-AzptYadbJ/1/BUrvLhvNyABN9OAKrtFYYyYMMB6lEV+ymgcoyt1NKgdd/DEUzr5LXbxBb9rJIm9GsDSIyrnuCg==",+      "peer": true,+      "peerDependencies": {+        "@babylonjs/core": "^9.0.0"+      }+    },+    "node_modules/@babylonjs/gui-editor": {+      "version": "9.5.0",+      "resolved": "https://registry.npmjs.org/@babylonjs/gui-editor/-/gui-editor-9.5.0.tgz",+      "integrity": "sha512-RrmrimK7UhOGGLDCgBJvwgFZ4ytmK9pfnCooj+lUgih0rpZM/FhyHrwJNZM445jDB9xlygPDUwtw73VdxmE1hA==",+      "peer": true,+      "peerDependencies": {+        "@babylonjs/core": "^9.0.0",+        "@babylonjs/gui": "^9.0.0",+        "@types/react": ">=16.7.3",+        "@types/react-dom": ">=16.0.9"+      }+    },+    "node_modules/@babylonjs/inspector": {+      "version": "9.5.0",+      "resolved": "https://registry.npmjs.org/@babylonjs/inspector/-/inspector-9.5.0.tgz",+      "integrity": "sha512-YP6LgiUJShwotk7t9iYQdU3qVfWmIiwZ3zJi0Ci3A4T0enulaf6dM1eK31gImp1mH01N3ZcnvTJ5y0UZ34mVog==",+      "bin": {+        "babylon-inspector": "bin/inspector-cli.mjs"+      },+      "peerDependencies": {+        "@babylonjs/addons": "^9.0.0",+        "@babylonjs/core": "^9.0.0",+        "@babylonjs/gui": "^9.0.0",+        "@babylonjs/gui-editor": "^9.0.0",+        "@babylonjs/loaders": "^9.0.0",+        "@babylonjs/materials": "^9.0.0",+        "@babylonjs/serializers": "^9.0.0",+        "@fluentui-contrib/react-resize-handle": "^0.8.4",+        "@fluentui-contrib/react-virtualizer": "^1.0.0",+        "@fluentui/react-components": "^9.70.0",+        "@fluentui/react-icons": "^2.0.310",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0",+        "usehooks-ts": "^3.1.1"+      }+    },+    "node_modules/@babylonjs/loaders": {+      "version": "9.5.0",+      "resolved": "https://registry.npmjs.org/@babylonjs/loaders/-/loaders-9.5.0.tgz",+      "integrity": "sha512-e2PqqPQ2A6xCJ6yW2NtcCzCKfN7neJjpxCUWBXqVUc268QnC1DNV49PM6bVpzNzTqzYjapMR9u5apcvAWo2iYQ==",+      "peerDependencies": {+        "@babylonjs/core": "^9.0.0",+        "babylonjs-gltf2interface": "^9.0.0"+      }+    },+    "node_modules/@babylonjs/materials": {+      "version": "9.5.0",+      "resolved": "https://registry.npmjs.org/@babylonjs/materials/-/materials-9.5.0.tgz",+      "integrity": "sha512-zKXs5UBqMGhDcpGhlIKvOuMPd77Pm6myAv0xEi0d7UL4dGk0u86bs4dQO2kqXGstf3bwzR3fNvljAuhOrDYflg==",+      "peer": true,+      "peerDependencies": {+        "@babylonjs/core": "^9.0.0"+      }+    },+    "node_modules/@babylonjs/serializers": {+      "version": "9.5.0",+      "resolved": "https://registry.npmjs.org/@babylonjs/serializers/-/serializers-9.5.0.tgz",+      "integrity": "sha512-ODbpEUnNUPFDMajkMqYNdVmogbDJoYA+ZAk0MEPDd/95xBf8rEfyyq4q7Y0ifVo6Vr4uJYyrFw02O1teIRYuoA==",+      "peer": true,+      "peerDependencies": {+        "@babylonjs/core": "^9.0.0",+        "babylonjs-gltf2interface": "^9.0.0"+      }+    },+    "node_modules/@ctrl/tinycolor": {+      "version": "3.6.1",+      "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",+      "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",+      "peer": true,+      "engines": {+        "node": ">=10"+      }+    },+    "node_modules/@emotion/hash": {+      "version": "0.9.2",+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",+      "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",+      "peer": true+    },+    "node_modules/@esbuild/aix-ppc64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",+      "cpu": [+        "ppc64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "aix"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/android-arm": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",+      "cpu": [+        "arm"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "android"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/android-arm64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",+      "cpu": [+        "arm64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "android"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/android-x64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",+      "cpu": [+        "x64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "android"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/darwin-arm64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",+      "cpu": [+        "arm64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "darwin"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/darwin-x64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",+      "cpu": [+        "x64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "darwin"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/freebsd-arm64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",+      "cpu": [+        "arm64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "freebsd"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/freebsd-x64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",+      "cpu": [+        "x64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "freebsd"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/linux-arm": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",+      "cpu": [+        "arm"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/linux-arm64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",+      "cpu": [+        "arm64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/linux-ia32": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",+      "cpu": [+        "ia32"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/linux-loong64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",+      "cpu": [+        "loong64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/linux-mips64el": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",+      "cpu": [+        "mips64el"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/linux-ppc64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",+      "cpu": [+        "ppc64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/linux-riscv64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",+      "cpu": [+        "riscv64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/linux-s390x": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",+      "cpu": [+        "s390x"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/linux-x64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",+      "cpu": [+        "x64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/netbsd-x64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",+      "cpu": [+        "x64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "netbsd"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/openbsd-x64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",+      "cpu": [+        "x64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "openbsd"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/sunos-x64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",+      "cpu": [+        "x64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "sunos"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/win32-arm64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",+      "cpu": [+        "arm64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "win32"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/win32-ia32": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",+      "cpu": [+        "ia32"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "win32"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@esbuild/win32-x64": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",+      "cpu": [+        "x64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "win32"+      ],+      "engines": {+        "node": ">=12"+      }+    },+    "node_modules/@floating-ui/core": {+      "version": "1.7.5",+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",+      "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",+      "peer": true,+      "dependencies": {+        "@floating-ui/utils": "^0.2.11"+      }+    },+    "node_modules/@floating-ui/devtools": {+      "version": "0.2.3",+      "resolved": "https://registry.npmjs.org/@floating-ui/devtools/-/devtools-0.2.3.tgz",+      "integrity": "sha512-ZTcxTvgo9CRlP7vJV62yCxdqmahHTGpSTi5QaTDgGoyQq0OyjaVZhUhXv/qdkQFOI3Sxlfmz0XGG4HaZMsDf8Q==",+      "peer": true,+      "peerDependencies": {+        "@floating-ui/dom": "^1.0.0"+      }+    },+    "node_modules/@floating-ui/dom": {+      "version": "1.7.6",+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",+      "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",+      "peer": true,+      "dependencies": {+        "@floating-ui/core": "^1.7.5",+        "@floating-ui/utils": "^0.2.11"+      }+    },+    "node_modules/@floating-ui/utils": {+      "version": "0.2.11",+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",+      "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",+      "peer": true+    },+    "node_modules/@fluentui-contrib/react-resize-handle": {+      "version": "0.8.4",+      "resolved": "https://registry.npmjs.org/@fluentui-contrib/react-resize-handle/-/react-resize-handle-0.8.4.tgz",+      "integrity": "sha512-g3cJ3q+nn32BB5b5mEtuSh6sGNYOGcKvRxQljvfg+UAhStceCPZYRM8gF+HswPPhQ0LUR6h780WMe072ndR6Lw==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-utilities": "^9.16.0",+        "@swc/helpers": "~0.5.11"+      },+      "peerDependencies": {+        "@fluentui/react-components": ">=9.70.0 <10.0.0",+        "@types/react": ">=16.8.0 <20.0.0",+        "@types/react-dom": ">=16.8.0 <20.0.0",+        "react": ">=16.8.0 <20.0.0",+        "react-dom": ">=16.8.0 <20.0.0"+      }+    },+    "node_modules/@fluentui-contrib/react-virtualizer": {+      "version": "1.0.0",+      "resolved": "https://registry.npmjs.org/@fluentui-contrib/react-virtualizer/-/react-virtualizer-1.0.0.tgz",+      "integrity": "sha512-z1KE+OyCTICkOeyCmFI0nlNRTPAhCv2ZhCIDjebnlkNQMIcMtTrI/ogy4Fu8UgbFP+WReE50JeduLtM+Sst+1A==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-jsx-runtime": "^9.0.29",+        "@fluentui/react-utilities": "^9.16.0",+        "@griffel/react": "^1.5.14",+        "@swc/helpers": "~0.5.11"+      },+      "peerDependencies": {+        "@fluentui/react-shared-contexts": ">=9.7.2 <10.0.0",+        "@types/react": ">=16.8.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.8.0 <20.0.0",+        "react-dom": ">=16.8.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/keyboard-keys": {+      "version": "9.0.8",+      "resolved": "https://registry.npmjs.org/@fluentui/keyboard-keys/-/keyboard-keys-9.0.8.tgz",+      "integrity": "sha512-iUSJUUHAyTosnXK8O2Ilbfxma+ZyZPMua5vB028Ys96z80v+LFwntoehlFsdH3rMuPsA8GaC1RE7LMezwPBPdw==",+      "peer": true,+      "dependencies": {+        "@swc/helpers": "^0.5.1"+      }+    },+    "node_modules/@fluentui/priority-overflow": {+      "version": "9.3.0",+      "resolved": "https://registry.npmjs.org/@fluentui/priority-overflow/-/priority-overflow-9.3.0.tgz",+      "integrity": "sha512-yaBC0R4e+4ZlCWDulB5S+xBrlnLwfzdg68GaarCqQO8OHjLg7Ah05xTj7PsAYcoHeEg/9vYeBwGXBpRO8+Tjqw==",+      "peer": true,+      "dependencies": {+        "@swc/helpers": "^0.5.1"+      }+    },+    "node_modules/@fluentui/react-accordion": {+      "version": "9.11.0",+      "resolved": "https://registry.npmjs.org/@fluentui/react-accordion/-/react-accordion-9.11.0.tgz",+      "integrity": "sha512-mEy73hbJM53tMj3MWqm3ajbBxj48uubnJjumVKI8Z/eXHS8L3GzUy5rf/gUH26xSR2Tl+edpFhYB8PFbJDIKKw==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-motion": "^9.15.0",+        "@fluentui/react-motion-components-preview": "^0.15.4",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-alert": {+      "version": "9.0.0-beta.139",+      "resolved": "https://registry.npmjs.org/@fluentui/react-alert/-/react-alert-9.0.0-beta.139.tgz",+      "integrity": "sha512-R9r4dwwpWpgFmB8wVeWqipjUh/e6lyacnerX39HtVdgcG/PE+kpdHjKGiy8MAD+BGYCzrUxKNhXTQDlpXasJ1Q==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-avatar": "^9.11.1",+        "@fluentui/react-button": "^9.9.1",+        "@fluentui/react-icons": "^2.0.239",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-aria": {+      "version": "9.17.11",+      "resolved": "https://registry.npmjs.org/@fluentui/react-aria/-/react-aria-9.17.11.tgz",+      "integrity": "sha512-K9nz+Wn5JliCpG6bIYYPXvKmpOql+w9uyzmYNYkYQ6QHgoCpph7XUFx1HCtsJm2PPNi8WO8g0ZV9jojdGKl1Tg==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-utilities": "^9.26.3",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-avatar": {+      "version": "9.11.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-avatar/-/react-avatar-9.11.1.tgz",+      "integrity": "sha512-y1T67rVQQ/D4FAod8F4crXo9funaptscRIiW81LAsbN82fFVexMPQ9GmXooQQvn6ILvjJtf9IyvSJ195qDsyag==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-badge": "^9.5.2",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-popover": "^9.14.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-tooltip": "^9.10.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-badge": {+      "version": "9.5.2",+      "resolved": "https://registry.npmjs.org/@fluentui/react-badge/-/react-badge-9.5.2.tgz",+      "integrity": "sha512-+UPAK9dCD6Gx+LWr6vqKMIbYOPf7oXX+GXRtCJ5fekCTHD0VgIWuIMuEtxVrHpJQdb2VNaZadY8/dMomk2JaXw==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-breadcrumb": {+      "version": "9.4.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-breadcrumb/-/react-breadcrumb-9.4.1.tgz",+      "integrity": "sha512-XgUB1yv04GdcL/6kUo6kh+BaN4df1A/Ds/fL1QxNrm5E26Vmvvlc0LN0WV/qb5qhKx0NwhtIXgOZHjfzyt7iCA==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-button": "^9.9.1",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-link": "^9.8.1",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-button": {+      "version": "9.9.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-button/-/react-button-9.9.1.tgz",+      "integrity": "sha512-WNzpseiVbqEKKevTkAnyHNoK/8ktYPE6rvf31gGvSDnBBclqfrn4PSYG2ppi+Z7abmClnaNFxpp1OHuOoVQ8Bg==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-card": {+      "version": "9.6.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-card/-/react-card-9.6.1.tgz",+      "integrity": "sha512-KBijjAxi0mBDSgnA1OCglqAVWc+Q0L7A2wCokszX/53oqfJPSvWxWFma7esz9b5MF/kdRrAR0vmy7MiosepNLQ==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-text": "^9.6.16",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-carousel": {+      "version": "9.9.7",+      "resolved": "https://registry.npmjs.org/@fluentui/react-carousel/-/react-carousel-9.9.7.tgz",+      "integrity": "sha512-lummYk+tASL/rM/SXWruoqhUAyJjTiOMgiCz55ncE3q2pSZe/EbsV5WfRw5B3y7pHX8xLusN831TBgUthj/sUw==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-button": "^9.9.1",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-tooltip": "^9.10.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1",+        "embla-carousel": "^8.5.1",+        "embla-carousel-autoplay": "^8.5.1",+        "embla-carousel-fade": "^8.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-checkbox": {+      "version": "9.6.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-checkbox/-/react-checkbox-9.6.1.tgz",+      "integrity": "sha512-Rsf3TmcNrzLuHan9lyUFUmMZnNyvS7DV8C4Vc9lZnZTFRBo94GRMGzu0BcWKFbr3cCDT/r5RmIyQYz0kc7Jd2w==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-field": "^9.5.1",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-label": "^9.4.1",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-color-picker": {+      "version": "9.2.16",+      "resolved": "https://registry.npmjs.org/@fluentui/react-color-picker/-/react-color-picker-9.2.16.tgz",+      "integrity": "sha512-+H8Ea8dwoSeUCTLRpUiGLrRsNvBnlHplnwJPU0isp8jdAfrIM/savZTLj6o4rqNFpNHQqAXxGwNuUV9YfHoJuQ==",+      "peer": true,+      "dependencies": {+        "@ctrl/tinycolor": "^3.3.4",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-combobox": {+      "version": "9.17.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-combobox/-/react-combobox-9.17.1.tgz",+      "integrity": "sha512-ezgt6tfOKd3wlG6IHvWl0TPNPpfHRtnEwC2kuqHYH/r1nMNp9edFi8Ya3+1eM7oxai19XW0swt69GPwRu51FVQ==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-field": "^9.5.1",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-portal": "^9.8.12",+        "@fluentui/react-positioning": "^9.22.1",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-components": {+      "version": "9.73.8",+      "resolved": "https://registry.npmjs.org/@fluentui/react-components/-/react-components-9.73.8.tgz",+      "integrity": "sha512-JG4KQjEvRRfPlh4yt6Rv1/k87ydM2y49r5XPNCnuYHahA7kEM+dY8JdOI7n7FW8bdcvZ7qt4smDrQ2XcPfmxlA==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-accordion": "^9.11.0",+        "@fluentui/react-alert": "9.0.0-beta.139",+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-avatar": "^9.11.1",+        "@fluentui/react-badge": "^9.5.2",+        "@fluentui/react-breadcrumb": "^9.4.1",+        "@fluentui/react-button": "^9.9.1",+        "@fluentui/react-card": "^9.6.1",+        "@fluentui/react-carousel": "^9.9.7",+        "@fluentui/react-checkbox": "^9.6.1",+        "@fluentui/react-color-picker": "^9.2.16",+        "@fluentui/react-combobox": "^9.17.1",+        "@fluentui/react-dialog": "^9.18.0",+        "@fluentui/react-divider": "^9.7.1",+        "@fluentui/react-drawer": "^9.12.0",+        "@fluentui/react-field": "^9.5.1",+        "@fluentui/react-image": "^9.4.1",+        "@fluentui/react-infobutton": "9.0.0-beta.115",+        "@fluentui/react-infolabel": "^9.4.20",+        "@fluentui/react-input": "^9.8.2",+        "@fluentui/react-label": "^9.4.1",+        "@fluentui/react-link": "^9.8.1",+        "@fluentui/react-list": "^9.6.14",+        "@fluentui/react-menu": "^9.24.1",+        "@fluentui/react-message-bar": "^9.7.0",+        "@fluentui/react-motion": "^9.15.0",+        "@fluentui/react-nav": "^9.3.24",+        "@fluentui/react-overflow": "^9.7.2",+        "@fluentui/react-persona": "^9.7.3",+        "@fluentui/react-popover": "^9.14.2",+        "@fluentui/react-portal": "^9.8.12",+        "@fluentui/react-positioning": "^9.22.1",+        "@fluentui/react-progress": "^9.5.1",+        "@fluentui/react-provider": "^9.22.16",+        "@fluentui/react-radio": "^9.6.2",+        "@fluentui/react-rating": "^9.4.1",+        "@fluentui/react-search": "^9.4.2",+        "@fluentui/react-select": "^9.5.1",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-skeleton": "^9.7.2",+        "@fluentui/react-slider": "^9.6.2",+        "@fluentui/react-spinbutton": "^9.6.2",+        "@fluentui/react-spinner": "^9.8.2",+        "@fluentui/react-swatch-picker": "^9.5.2",+        "@fluentui/react-switch": "^9.7.2",+        "@fluentui/react-table": "^9.19.15",+        "@fluentui/react-tabs": "^9.12.1",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-tag-picker": "^9.8.6",+        "@fluentui/react-tags": "^9.8.1",+        "@fluentui/react-teaching-popover": "^9.6.21",+        "@fluentui/react-text": "^9.6.16",+        "@fluentui/react-textarea": "^9.7.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-toast": "^9.7.17",+        "@fluentui/react-toolbar": "^9.8.0",+        "@fluentui/react-tooltip": "^9.10.1",+        "@fluentui/react-tree": "^9.16.0",+        "@fluentui/react-utilities": "^9.26.3",+        "@fluentui/react-virtualizer": "9.0.0-alpha.112",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-context-selector": {+      "version": "9.2.16",+      "resolved": "https://registry.npmjs.org/@fluentui/react-context-selector/-/react-context-selector-9.2.16.tgz",+      "integrity": "sha512-D+/X2liT+eZe0rzXbwddPH333ml2SXz71biR13aeyGJQr8+W+icMAIsYhpwk0CC3KtJ3f1/CLTm7vcIrvqsJ4g==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-utilities": "^9.26.3",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0",+        "scheduler": ">=0.19.0"+      }+    },+    "node_modules/@fluentui/react-dialog": {+      "version": "9.18.0",+      "resolved": "https://registry.npmjs.org/@fluentui/react-dialog/-/react-dialog-9.18.0.tgz",+      "integrity": "sha512-i+V2o0NJ1itjVADJFov5AR/JetpD2hCMiLye0vfi3/XsFMgEPZnGzILVxPCO/ovULTiCyThcL1UvY0d/PYrZfA==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-motion": "^9.15.0",+        "@fluentui/react-motion-components-preview": "^0.15.4",+        "@fluentui/react-portal": "^9.8.12",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-divider": {+      "version": "9.7.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-divider/-/react-divider-9.7.1.tgz",+      "integrity": "sha512-ptymE6iADb/ugezulaMeoAfGxKSwOjHEHBh8N1ydOR3AoOxsSUPkvoPC0mReO/yV5Nas7pz5s5VuJTspmFz0hA==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-drawer": {+      "version": "9.12.0",+      "resolved": "https://registry.npmjs.org/@fluentui/react-drawer/-/react-drawer-9.12.0.tgz",+      "integrity": "sha512-PUXeXUH6JqwpjqYphHesHl75UAFSvxQJQqrevMFHE78ZF0Cqn59Xpa+8hGwRSuoRcYa90jjfHzJOOjN0iNM2iA==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-dialog": "^9.18.0",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-motion": "^9.15.0",+        "@fluentui/react-motion-components-preview": "^0.15.4",+        "@fluentui/react-portal": "^9.8.12",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-field": {+      "version": "9.5.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-field/-/react-field-9.5.1.tgz",+      "integrity": "sha512-u8J2d3AWb4yZXvy/mQd95y2lTon890RfybBTCbeBUzApGMI/77WqT5pRJ+zTM3lOMToPHVKylchNFusMpJaX9w==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-label": "^9.4.1",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-icons": {+      "version": "2.0.325",+      "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.325.tgz",+      "integrity": "sha512-dbmpGMwyCFfkFdMSTEFnbNMXKDomPpqbzXVKRKM0EEQrcwOoQvm08d+SV+WWH11kni26SrgEtB9ycldD+fr92w==",+      "peer": true,+      "dependencies": {+        "@griffel/react": "^1.6.1",+        "tslib": "^2.1.0"+      },+      "peerDependencies": {+        "react": ">=16.8.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-image": {+      "version": "9.4.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-image/-/react-image-9.4.1.tgz",+      "integrity": "sha512-yNd2Wq2xq952UUEVBkWeEmM7bTKdWx6BnsHPYRf0kdTADox2PquApYXsI1xw2pnAh3GSjARrGi9Eto0qxouLqA==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-infobutton": {+      "version": "9.0.0-beta.115",+      "resolved": "https://registry.npmjs.org/@fluentui/react-infobutton/-/react-infobutton-9.0.0-beta.115.tgz",+      "integrity": "sha512-b+4B0ODzPEb4jNaW9HdT6VVt3CL5FgPL2yuKzALBsYVl3udJdFpyxHsZEPf3JrVTBL/rgF2fRI1iAioX6Fl7DA==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-icons": "^2.0.237",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-label": "^9.4.1",+        "@fluentui/react-popover": "^9.14.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-infolabel": {+      "version": "9.4.20",+      "resolved": "https://registry.npmjs.org/@fluentui/react-infolabel/-/react-infolabel-9.4.20.tgz",+      "integrity": "sha512-w4FOnNP+CtbVdKBEO6wXAcmOuPZWvmB/BJj+7J/8cLAQm7+4kQgitFHncU6rtFhPdGbikVoBf707/0R1mA4aIg==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-label": "^9.4.1",+        "@fluentui/react-popover": "^9.14.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.8.0 <20.0.0",+        "@types/react-dom": ">=16.8.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.8.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-input": {+      "version": "9.8.2",+      "resolved": "https://registry.npmjs.org/@fluentui/react-input/-/react-input-9.8.2.tgz",+      "integrity": "sha512-t9zmqZR4bqeRjpWuCGfI4yrtPoCXFiK2XO4BoV5nNwAesglgz4+Vtso4YXst9QYEAazHtKI73YFJf1mn55hCuA==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-field": "^9.5.1",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-jsx-runtime": {+      "version": "9.4.2",+      "resolved": "https://registry.npmjs.org/@fluentui/react-jsx-runtime/-/react-jsx-runtime-9.4.2.tgz",+      "integrity": "sha512-y3o0PBg2qzSdvgxDm7rH9BWq7E1h/eUWS+IhjQhd9dRpme6Py01+OLOglHojM5Tc9QjIp2Rjy2mFWBHXOR+8mw==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-utilities": "^9.26.3",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-label": {+      "version": "9.4.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-label/-/react-label-9.4.1.tgz",+      "integrity": "sha512-4O3cPX6dSJVBKlIEbznjJ08utEc98lKbZz/6MZTTQfFgYl0TxAhxEDsIIIyNjj0Xy9eJpqubJsaswucWXTG/qg==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-link": {+      "version": "9.8.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-link/-/react-link-9.8.1.tgz",+      "integrity": "sha512-ZxrCeX4pMWHujdmYV8b0QW0ztLtu0rHHvRNx67Y3WqSijVyij8QtNNiZ/nab+UDNlz9t8QIXKdWQgYj1uKDpMg==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-list": {+      "version": "9.6.14",+      "resolved": "https://registry.npmjs.org/@fluentui/react-list/-/react-list-9.6.14.tgz",+      "integrity": "sha512-B1mUQFvJOUlZysSduVnATNZggrGpgEWnW9ZSJAZ17LM0+9nWEQRi40jpUGI/d3PGKHt5O2df78s+1nEPAk0L6A==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-checkbox": "^9.6.1",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.8.0 <20.0.0",+        "@types/react-dom": ">=16.8.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.8.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-menu": {+      "version": "9.24.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-menu/-/react-menu-9.24.1.tgz",+      "integrity": "sha512-NLB5EhzKFiwax3O5JTRTtsqdEFDGEXzEuP/suyxNAaaQsIuXygo//Rmdq6dSn7GybTpEOZHKxYDyyG7dj+a4YA==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-motion": "^9.15.0",+        "@fluentui/react-motion-components-preview": "^0.15.4",+        "@fluentui/react-portal": "^9.8.12",+        "@fluentui/react-positioning": "^9.22.1",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-message-bar": {+      "version": "9.7.0",+      "resolved": "https://registry.npmjs.org/@fluentui/react-message-bar/-/react-message-bar-9.7.0.tgz",+      "integrity": "sha512-ICFDxZ62r5OG97/FcfK1EfJPxGlyDNyFixLD/a3gOREvEcT/hyZgnlUM9Y30u92HjxChx2SwGWnv3iaQPsvToQ==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-button": "^9.9.1",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-link": "^9.8.1",+        "@fluentui/react-motion": "^9.15.0",+        "@fluentui/react-motion-components-preview": "^0.15.4",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.8.0 <20.0.0",+        "@types/react-dom": ">=16.8.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.8.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-motion": {+      "version": "9.15.0",+      "resolved": "https://registry.npmjs.org/@fluentui/react-motion/-/react-motion-9.15.0.tgz",+      "integrity": "sha512-ZNQHYzE6MRbLQFT08/mrcqQ9k7F5niktRP93X1v/kmwKfPjvdDofySfbhQXQs3zQw600690C9rfJTKUd3h+zlg==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-utilities": "^9.26.3",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.8.0 <20.0.0",+        "@types/react-dom": ">=16.8.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.8.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-motion-components-preview": {+      "version": "0.15.4",+      "resolved": "https://registry.npmjs.org/@fluentui/react-motion-components-preview/-/react-motion-components-preview-0.15.4.tgz",+      "integrity": "sha512-gAHPlyEYylZzUSGwc68VaB+vO8CTX6tgIA3d2+jFrpcwvXZjsdCpF1w1zK1+hTuiipmEaZLZyBz0e0CKH2+3XQ==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-motion": "*",+        "@fluentui/react-utilities": "*",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-nav": {+      "version": "9.3.24",+      "resolved": "https://registry.npmjs.org/@fluentui/react-nav/-/react-nav-9.3.24.tgz",+      "integrity": "sha512-OlB5k5Zev5VNjSRfJvJLO09Hjcv2UHAjLpSVa6gKHx+1NqqSJWZeDLSF7r+/nyZ4CWP5jWZYq7whEu3WvzdVZw==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-button": "^9.9.1",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-divider": "^9.7.1",+        "@fluentui/react-drawer": "^9.12.0",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-motion": "^9.15.0",+        "@fluentui/react-motion-components-preview": "^0.15.4",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-tooltip": "^9.10.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-overflow": {+      "version": "9.7.2",+      "resolved": "https://registry.npmjs.org/@fluentui/react-overflow/-/react-overflow-9.7.2.tgz",+      "integrity": "sha512-5PA67LgnVmbbOzBN2H5gH3OvSVy1373VJfsHq2+6TLCfm+LXAkWBoFwvBuFI7HsMYae9A0FVlgX7gTsKVfMddw==",+      "peer": true,+      "dependencies": {+        "@fluentui/priority-overflow": "^9.3.0",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-persona": {+      "version": "9.7.3",+      "resolved": "https://registry.npmjs.org/@fluentui/react-persona/-/react-persona-9.7.3.tgz",+      "integrity": "sha512-OY3xpSD6l4NDdeKihriC+H0q6P1CA2xyZ+pe/WwfKPnatxs2BALoRFtDQduMO7AK/j0w7UAxnaZrvEeftLen2g==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-avatar": "^9.11.1",+        "@fluentui/react-badge": "^9.5.2",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-popover": {+      "version": "9.14.2",+      "resolved": "https://registry.npmjs.org/@fluentui/react-popover/-/react-popover-9.14.2.tgz",+      "integrity": "sha512-EDvzLkT98/vcCSGrcZWUACGsvLjrHin0Xf9eowMQKiiHFWbu8HNRmr7W2XB9Eja1W5HSIK6+mV8ro9zrLibG4w==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-motion": "^9.15.0",+        "@fluentui/react-motion-components-preview": "^0.15.4",+        "@fluentui/react-portal": "^9.8.12",+        "@fluentui/react-positioning": "^9.22.1",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-portal": {+      "version": "9.8.12",+      "resolved": "https://registry.npmjs.org/@fluentui/react-portal/-/react-portal-9.8.12.tgz",+      "integrity": "sha512-+WH0wH/5lsodGP6Mam1alHXpkMCYA5uMcnF98RVOs7/GR69KiFcza1mCnvPJUaJ55AfwLuz/xLxuWdWgQnUdMQ==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-positioning": {+      "version": "9.22.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-positioning/-/react-positioning-9.22.1.tgz",+      "integrity": "sha512-/r1BHQKr/WCjEM8UGloiq7bWWBSYB/Uqt7D1sAF9EHd968VH07cAN3RMVKmWWjeJO31rstOZHdgcz0WHhFF+2Q==",+      "peer": true,+      "dependencies": {+        "@floating-ui/devtools": "^0.2.3",+        "@floating-ui/dom": "^1.6.12",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1",+        "use-sync-external-store": "^1.2.0"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-progress": {+      "version": "9.5.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-progress/-/react-progress-9.5.1.tgz",+      "integrity": "sha512-EXJ/Bp67d5+bXPNpPabxdtXUgCMTtvYrBoKtIS6wE5KeUzaek/rgQ3v5wnGfbuLnJ4J/kj+n7XQEc9fhoFPy9w==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-field": "^9.5.1",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-motion": "^9.15.0",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-provider": {+      "version": "9.22.16",+      "resolved": "https://registry.npmjs.org/@fluentui/react-provider/-/react-provider-9.22.16.tgz",+      "integrity": "sha512-S77n5ASUWE/V1I6lX09CrHm4TAKSGENhIrKz9qMKDv2Vrq44/j3eGBLz12k8IW4TJVu9nwGwst9kBpCT+3WHpA==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/core": "^1.16.0",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-radio": {+      "version": "9.6.2",+      "resolved": "https://registry.npmjs.org/@fluentui/react-radio/-/react-radio-9.6.2.tgz",+      "integrity": "sha512-Sp2us4eWRopUKOMCQw5/iks7euPKY6FeesBCCUIVGBg5VKZf2/CfEtbCa9hMjn4D4PCHGivnUTf23t238mvvnw==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-field": "^9.5.1",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-label": "^9.4.1",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-rating": {+      "version": "9.4.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-rating/-/react-rating-9.4.1.tgz",+      "integrity": "sha512-DfWipzrT44j+yaShtfHz96/vHEa5ut5IR1kobrO0bSqAcpetOn327gFeY+sG/W6xzork/STcy/T836yK8A2+DQ==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.8.0 <20.0.0",+        "@types/react-dom": ">=16.8.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.8.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-search": {+      "version": "9.4.2",+      "resolved": "https://registry.npmjs.org/@fluentui/react-search/-/react-search-9.4.2.tgz",+      "integrity": "sha512-PIb50euHoMsKWLqFymf8wo/+z1jrx1MB7uNuhjNT5DvwTP4VYAy5EtRCSwVRyxWNSaWSL6iy6dDy517EQE96mA==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-input": "^9.8.2",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-select": {+      "version": "9.5.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-select/-/react-select-9.5.1.tgz",+      "integrity": "sha512-8GocQKiUHEUlAks6zA0HbGGSF2lpjuSZuxPzIBqTyuWof8vFiK6eFAcSXb0hTYIVH3RsTihhfc6G3NRnHoBrzg==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-field": "^9.5.1",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-shared-contexts": {+      "version": "9.26.2",+      "resolved": "https://registry.npmjs.org/@fluentui/react-shared-contexts/-/react-shared-contexts-9.26.2.tgz",+      "integrity": "sha512-upKXkwlIp5oIhELr4clAZXQkuCd4GDXM6GZEz8BOmRO+PnxyqmycCXvxDxsmi6XN+0vkGM4joiIgkB14o/FctQ==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-theme": "^9.2.1",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-skeleton": {+      "version": "9.7.2",+      "resolved": "https://registry.npmjs.org/@fluentui/react-skeleton/-/react-skeleton-9.7.2.tgz",+      "integrity": "sha512-PrUgdSGDAZw9FIP5NyvPoPfHDe2N9VxMyBfyTwWfZVg03dzRfnE3vEqr7N5xyfv4JsRs6u1xSqVn/0jdS0IEMQ==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-field": "^9.5.1",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-slider": {+      "version": "9.6.2",+      "resolved": "https://registry.npmjs.org/@fluentui/react-slider/-/react-slider-9.6.2.tgz",+      "integrity": "sha512-lVavtTg8eqovfRokeYDk4popwCi8nuicacJ/HZdF3ni5e3y/2WT/bVP0eErS/GvC7+90ACQQ8uxdr4sjjY/HWA==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-field": "^9.5.1",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-spinbutton": {+      "version": "9.6.2",+      "resolved": "https://registry.npmjs.org/@fluentui/react-spinbutton/-/react-spinbutton-9.6.2.tgz",+      "integrity": "sha512-P4vvJH7P5yHPFAv6aSo3dZxtErN62DiRJN+nEKS+/XBoRGsOGQdqyyx5Q/PQKOmyQrtwuZdXNHUjcyv8b50T2w==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-field": "^9.5.1",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-spinner": {+      "version": "9.8.2",+      "resolved": "https://registry.npmjs.org/@fluentui/react-spinner/-/react-spinner-9.8.2.tgz",+      "integrity": "sha512-0LxykLJGUD/I3XEeIXAWznwdg9XRe0piaByR0nLFOOV3UPwkVc2w5UdPhy2Y0NZDvtPHbNaMCuQAq82+bxg/0w==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-label": "^9.4.1",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-swatch-picker": {+      "version": "9.5.2",+      "resolved": "https://registry.npmjs.org/@fluentui/react-swatch-picker/-/react-swatch-picker-9.5.2.tgz",+      "integrity": "sha512-DK6UU9OJY9XaGBPU2ROx+B5/7XdwVtHBdVthOAptyKSsYGOdQt5AQqg3ZOXH6r5WYbMRQDuP2OZ2iKtwidFCVQ==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-field": "^9.5.1",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.8.0 <20.0.0",+        "@types/react-dom": ">=16.8.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.8.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-switch": {+      "version": "9.7.2",+      "resolved": "https://registry.npmjs.org/@fluentui/react-switch/-/react-switch-9.7.2.tgz",+      "integrity": "sha512-j3e5se+3d+befV9MytkxxvJ9nHZOeZ7thKDTF4YVSYf6kcNx9eOlLvPgDjhGO08gzngO4B7aaprhDN7DJc3W1g==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-field": "^9.5.1",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-label": "^9.4.1",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-table": {+      "version": "9.19.15",+      "resolved": "https://registry.npmjs.org/@fluentui/react-table/-/react-table-9.19.15.tgz",+      "integrity": "sha512-OdQ2Nwx2nAlPMlJeyAFrKa3Zy5Ya/H87OU8MvtFJhabM/FkHiZoli/DO1mavVI+jqavOlJuQWmJ55D6jjuGa7g==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-avatar": "^9.11.1",+        "@fluentui/react-checkbox": "^9.6.1",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-radio": "^9.6.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-tabs": {+      "version": "9.12.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-tabs/-/react-tabs-9.12.1.tgz",+      "integrity": "sha512-WvzOtpC6C/7Mo5X+xmE+3stpCbx2iH9BqrEN5KuGrsHJ78DjMDeabYeL90vlrHBdP4VlTpwdORBui/jtWkxnmQ==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-tabster": {+      "version": "9.26.14",+      "resolved": "https://registry.npmjs.org/@fluentui/react-tabster/-/react-tabster-9.26.14.tgz",+      "integrity": "sha512-WibgoF67hl6BXfmsY6RSIWSHadeMP/6EDG9gAacfHlwKvK0+FiHp5ernwuXTPAmu2kiHicn2qUZ8EteCFiFryg==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1",+        "keyborg": "^2.6.0",+        "tabster": "^8.5.5"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-tag-picker": {+      "version": "9.8.6",+      "resolved": "https://registry.npmjs.org/@fluentui/react-tag-picker/-/react-tag-picker-9.8.6.tgz",+      "integrity": "sha512-sOZ+wBA3hgGhKrOP7wbjB2yRvAxjcRXtcj1jDTrtSkaDPXb3K0nGmjiqp2mve995ps3wvCGnNKK4EurX842ZbA==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-combobox": "^9.17.1",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-field": "^9.5.1",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-portal": "^9.8.12",+        "@fluentui/react-positioning": "^9.22.1",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-tags": "^9.8.1",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-tags": {+      "version": "9.8.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-tags/-/react-tags-9.8.1.tgz",+      "integrity": "sha512-6ZTW78fu5eWByKHIM3i+raDrX3hwfZ67ONfZ8wEUXfZHowskxqpMHI8Gw7IAMWkC1scgLqEnht8TnHvZgjo7Ug==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-avatar": "^9.11.1",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-teaching-popover": {+      "version": "9.6.21",+      "resolved": "https://registry.npmjs.org/@fluentui/react-teaching-popover/-/react-teaching-popover-9.6.21.tgz",+      "integrity": "sha512-V86zLB1B8xu3U/02FvvMdsJP+ZC9l3vT9bQ2Gr7hZHxJ4/0NLpVcrYSBFHpON9e/WK3p+A5b5V96p86b5Pavlg==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-button": "^9.9.1",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-popover": "^9.14.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1",+        "use-sync-external-store": "^1.2.0"+      },+      "peerDependencies": {+        "@types/react": ">=16.8.0 <20.0.0",+        "@types/react-dom": ">=16.8.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.8.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-text": {+      "version": "9.6.16",+      "resolved": "https://registry.npmjs.org/@fluentui/react-text/-/react-text-9.6.16.tgz",+      "integrity": "sha512-ZzCSJWQ6LrVuPqA6sqNEZaXbLvhi2NxBOtlMudWlqYzidLQp038d7mMGSzNnhyeblg+gj+bOVE2eOgWFuVHGYw==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-textarea": {+      "version": "9.7.2",+      "resolved": "https://registry.npmjs.org/@fluentui/react-textarea/-/react-textarea-9.7.2.tgz",+      "integrity": "sha512-awlkZoW81WaOqSoXTT9rZs3mTAzCCHnC9eAm6J8ZxI5+ASX07BTolBfZ82it5wxOHI5GMDfbFOl+xIy8uAMdzA==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-field": "^9.5.1",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-theme": {+      "version": "9.2.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-theme/-/react-theme-9.2.1.tgz",+      "integrity": "sha512-lJxfz7LmmglFz+c9C41qmMqaRRZZUPtPPl9DWQ79vH+JwZd4dkN7eA78OTRwcGCOTPEKoLTX72R+EFaWEDlX+w==",+      "peer": true,+      "dependencies": {+        "@fluentui/tokens": "1.0.0-alpha.23",+        "@swc/helpers": "^0.5.1"+      }+    },+    "node_modules/@fluentui/react-toast": {+      "version": "9.7.17",+      "resolved": "https://registry.npmjs.org/@fluentui/react-toast/-/react-toast-9.7.17.tgz",+      "integrity": "sha512-DWA5EARWSo1k19iWAulLpKrcUHT+Dq/Bw9zfdpoQEWWybrAZwyN7WiYFkBjKCQRxSp10OjLASSQK91CXfb1wJA==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-motion": "^9.15.0",+        "@fluentui/react-motion-components-preview": "^0.15.4",+        "@fluentui/react-portal": "^9.8.12",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-toolbar": {+      "version": "9.8.0",+      "resolved": "https://registry.npmjs.org/@fluentui/react-toolbar/-/react-toolbar-9.8.0.tgz",+      "integrity": "sha512-EIe+QWOaFR1pZzENefsFTmjxGa2yJb4A/by3kGuGqSjx7isqPUllPq0/kFQzfoUYgPDJbJtQ+KyuRDekTL0QpQ==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-button": "^9.9.1",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-divider": "^9.7.1",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-radio": "^9.6.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-tooltip": {+      "version": "9.10.1",+      "resolved": "https://registry.npmjs.org/@fluentui/react-tooltip/-/react-tooltip-9.10.1.tgz",+      "integrity": "sha512-IPHBFjqGhaaMDhLt5NSNOE9LEpDOpT7qgEqNz+Mlflo0A4qI2LW/EnkNop7IRmX/bC88A+wUtEONTjjR87dNBw==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-portal": "^9.8.12",+        "@fluentui/react-positioning": "^9.22.1",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-tree": {+      "version": "9.16.0",+      "resolved": "https://registry.npmjs.org/@fluentui/react-tree/-/react-tree-9.16.0.tgz",+      "integrity": "sha512-c+Q4AVaYk9U69aGDgmJVNne+CtWKS75YIfGoxs6+9+wE2Wqz4T0E+gE1ng7ARCQQgI7E2NEJlot6DuI6nYYrRw==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-aria": "^9.17.11",+        "@fluentui/react-avatar": "^9.11.1",+        "@fluentui/react-button": "^9.9.1",+        "@fluentui/react-checkbox": "^9.6.1",+        "@fluentui/react-context-selector": "^9.2.16",+        "@fluentui/react-icons": "^2.0.245",+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-motion": "^9.15.0",+        "@fluentui/react-motion-components-preview": "^0.15.4",+        "@fluentui/react-radio": "^9.6.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-tabster": "^9.26.14",+        "@fluentui/react-theme": "^9.2.1",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-utilities": {+      "version": "9.26.3",+      "resolved": "https://registry.npmjs.org/@fluentui/react-utilities/-/react-utilities-9.26.3.tgz",+      "integrity": "sha512-bXB3jMm/RroT8c5eGZkijkPbLd4MqMI6biBHjavo0e7OkZHv9IPfH2nDkGhSn5Sh8e6kRcX0IjYhbM10WUK2iQ==",+      "peer": true,+      "dependencies": {+        "@fluentui/keyboard-keys": "^9.0.8",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/react-virtualizer": {+      "version": "9.0.0-alpha.112",+      "resolved": "https://registry.npmjs.org/@fluentui/react-virtualizer/-/react-virtualizer-9.0.0-alpha.112.tgz",+      "integrity": "sha512-dao/mQssaPFxCXMx7K+G/DrRoZg28kXcE1NGbJ1RPtbkVCzJgwrEEeDhM5/wyOXO/Z5EZ31FIerDDVOyr6FAaw==",+      "peer": true,+      "dependencies": {+        "@fluentui/react-jsx-runtime": "^9.4.2",+        "@fluentui/react-shared-contexts": "^9.26.2",+        "@fluentui/react-utilities": "^9.26.3",+        "@griffel/react": "^1.5.32",+        "@swc/helpers": "^0.5.1"+      },+      "peerDependencies": {+        "@types/react": ">=16.14.0 <20.0.0",+        "@types/react-dom": ">=16.9.0 <20.0.0",+        "react": ">=16.14.0 <20.0.0",+        "react-dom": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@fluentui/tokens": {+      "version": "1.0.0-alpha.23",+      "resolved": "https://registry.npmjs.org/@fluentui/tokens/-/tokens-1.0.0-alpha.23.tgz",+      "integrity": "sha512-uxrzF9Z+J10naP0pGS7zPmzSkspSS+3OJDmYIK3o1nkntQrgBXq3dBob4xSlTDm5aOQ0kw6EvB9wQgtlyy4eKQ==",+      "peer": true,+      "dependencies": {+        "@swc/helpers": "^0.5.1"+      }+    },+    "node_modules/@griffel/core": {+      "version": "1.20.3",+      "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.20.3.tgz",+      "integrity": "sha512-m810TeUVL+uq8WAFabAN7dT5gX49IvMN6dA+mR6wwXA6IynYe0NxcqUi+Kel6FScx5joSUBDnF3NxJXNNP+qDg==",+      "peer": true,+      "dependencies": {+        "@emotion/hash": "^0.9.0",+        "@griffel/style-types": "^1.4.0",+        "csstype": "^3.1.3",+        "rtl-css-js": "^1.16.1",+        "stylis": "^4.2.0",+        "tslib": "^2.1.0"+      }+    },+    "node_modules/@griffel/react": {+      "version": "1.7.1",+      "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.7.1.tgz",+      "integrity": "sha512-QJXjOi+U0JbgD5LaI0XcpTWLBXM6nSbCL+XmRuPHQWXoteMqJnjrb5Na8EUtf4xk0be8pcE34mY7CnxpfnQ6rw==",+      "peer": true,+      "dependencies": {+        "@griffel/core": "^1.20.3",+        "tslib": "^2.1.0"+      },+      "peerDependencies": {+        "react": ">=16.14.0 <20.0.0"+      }+    },+    "node_modules/@griffel/style-types": {+      "version": "1.4.0",+      "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.4.0.tgz",+      "integrity": "sha512-vNDfOGV7RN/XkA7vxgf7Z5HgW8eiBm5cHT9wQPhsKB4pxWom5u6eQ9CkYE5mCCTSPl9H6Nd1NBai04d4P6BD7Q==",+      "peer": true,+      "dependencies": {+        "csstype": "^3.1.3"+      }+    },+    "node_modules/@jridgewell/gen-mapping": {+      "version": "0.3.13",+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",+      "dependencies": {+        "@jridgewell/sourcemap-codec": "^1.5.0",+        "@jridgewell/trace-mapping": "^0.3.24"+      }+    },+    "node_modules/@jridgewell/resolve-uri": {+      "version": "3.1.2",+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",+      "engines": {+        "node": ">=6.0.0"+      }+    },+    "node_modules/@jridgewell/source-map": {+      "version": "0.3.11",+      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",+      "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",+      "dev": true,+      "dependencies": {+        "@jridgewell/gen-mapping": "^0.3.5",+        "@jridgewell/trace-mapping": "^0.3.25"+      }+    },+    "node_modules/@jridgewell/sourcemap-codec": {+      "version": "1.5.5",+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="+    },+    "node_modules/@jridgewell/trace-mapping": {+      "version": "0.3.31",+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",+      "dependencies": {+        "@jridgewell/resolve-uri": "^3.1.0",+        "@jridgewell/sourcemap-codec": "^1.4.14"+      }+    },+    "node_modules/@rollup/rollup-android-arm-eabi": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",+      "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",+      "cpu": [+        "arm"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "android"+      ]+    },+    "node_modules/@rollup/rollup-android-arm64": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",+      "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",+      "cpu": [+        "arm64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "android"+      ]+    },+    "node_modules/@rollup/rollup-darwin-arm64": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",+      "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",+      "cpu": [+        "arm64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "darwin"+      ]+    },+    "node_modules/@rollup/rollup-darwin-x64": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",+      "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",+      "cpu": [+        "x64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "darwin"+      ]+    },+    "node_modules/@rollup/rollup-freebsd-arm64": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",+      "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",+      "cpu": [+        "arm64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "freebsd"+      ]+    },+    "node_modules/@rollup/rollup-freebsd-x64": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",+      "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",+      "cpu": [+        "x64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "freebsd"+      ]+    },+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",+      "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",+      "cpu": [+        "arm"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ]+    },+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",+      "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",+      "cpu": [+        "arm"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ]+    },+    "node_modules/@rollup/rollup-linux-arm64-gnu": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",+      "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",+      "cpu": [+        "arm64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ]+    },+    "node_modules/@rollup/rollup-linux-arm64-musl": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",+      "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",+      "cpu": [+        "arm64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ]+    },+    "node_modules/@rollup/rollup-linux-loong64-gnu": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",+      "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",+      "cpu": [+        "loong64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ]+    },+    "node_modules/@rollup/rollup-linux-loong64-musl": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",+      "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",+      "cpu": [+        "loong64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ]+    },+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",+      "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",+      "cpu": [+        "ppc64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ]+    },+    "node_modules/@rollup/rollup-linux-ppc64-musl": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",+      "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",+      "cpu": [+        "ppc64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ]+    },+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",+      "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",+      "cpu": [+        "riscv64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ]+    },+    "node_modules/@rollup/rollup-linux-riscv64-musl": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",+      "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",+      "cpu": [+        "riscv64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ]+    },+    "node_modules/@rollup/rollup-linux-s390x-gnu": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",+      "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",+      "cpu": [+        "s390x"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ]+    },+    "node_modules/@rollup/rollup-linux-x64-gnu": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",+      "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",+      "cpu": [+        "x64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ]+    },+    "node_modules/@rollup/rollup-linux-x64-musl": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",+      "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",+      "cpu": [+        "x64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "linux"+      ]+    },+    "node_modules/@rollup/rollup-openbsd-x64": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",+      "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",+      "cpu": [+        "x64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "openbsd"+      ]+    },+    "node_modules/@rollup/rollup-openharmony-arm64": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",+      "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",+      "cpu": [+        "arm64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "openharmony"+      ]+    },+    "node_modules/@rollup/rollup-win32-arm64-msvc": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",+      "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",+      "cpu": [+        "arm64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "win32"+      ]+    },+    "node_modules/@rollup/rollup-win32-ia32-msvc": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",+      "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",+      "cpu": [+        "ia32"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "win32"+      ]+    },+    "node_modules/@rollup/rollup-win32-x64-gnu": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",+      "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",+      "cpu": [+        "x64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "win32"+      ]+    },+    "node_modules/@rollup/rollup-win32-x64-msvc": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",+      "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",+      "cpu": [+        "x64"+      ],+      "dev": true,+      "optional": true,+      "os": [+        "win32"+      ]+    },+    "node_modules/@sveltejs/vite-plugin-svelte": {+      "version": "3.1.2",+      "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.2.tgz",+      "integrity": "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==",+      "dev": true,+      "dependencies": {+        "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0",+        "debug": "^4.3.4",+        "deepmerge": "^4.3.1",+        "kleur": "^4.1.5",+        "magic-string": "^0.30.10",+        "svelte-hmr": "^0.16.0",+        "vitefu": "^0.2.5"+      },+      "engines": {+        "node": "^18.0.0 || >=20"+      },+      "peerDependencies": {+        "svelte": "^4.0.0 || ^5.0.0-next.0",+        "vite": "^5.0.0"+      }+    },+    "node_modules/@sveltejs/vite-plugin-svelte-inspector": {+      "version": "2.1.0",+      "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz",+      "integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==",+      "dev": true,+      "dependencies": {+        "debug": "^4.3.4"+      },+      "engines": {+        "node": "^18.0.0 || >=20"+      },+      "peerDependencies": {+        "@sveltejs/vite-plugin-svelte": "^3.0.0",+        "svelte": "^4.0.0 || ^5.0.0-next.0",+        "vite": "^5.0.0"+      }+    },+    "node_modules/@swc/helpers": {+      "version": "0.5.21",+      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz",+      "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==",+      "peer": true,+      "dependencies": {+        "tslib": "^2.8.0"+      }+    },+    "node_modules/@types/estree": {+      "version": "1.0.8",+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="+    },+    "node_modules/@types/pug": {+      "version": "2.0.10",+      "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",+      "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",+      "dev": true+    },+    "node_modules/@types/react": {+      "version": "19.2.14",+      "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",+      "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",+      "peer": true,+      "dependencies": {+        "csstype": "^3.2.2"+      }+    },+    "node_modules/@types/react-dom": {+      "version": "19.2.3",+      "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",+      "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",+      "peer": true,+      "peerDependencies": {+        "@types/react": "^19.2.0"+      }+    },+    "node_modules/acorn": {+      "version": "8.16.0",+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",+      "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",+      "bin": {+        "acorn": "bin/acorn"+      },+      "engines": {+        "node": ">=0.4.0"+      }+    },+    "node_modules/anymatch": {+      "version": "3.1.3",+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",+      "dev": true,+      "dependencies": {+        "normalize-path": "^3.0.0",+        "picomatch": "^2.0.4"+      },+      "engines": {+        "node": ">= 8"+      }+    },+    "node_modules/aria-query": {+      "version": "5.3.2",+      "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",+      "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",+      "engines": {+        "node": ">= 0.4"+      }+    },+    "node_modules/axobject-query": {+      "version": "4.1.0",+      "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",+      "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",+      "engines": {+        "node": ">= 0.4"+      }+    },+    "node_modules/babylonjs-gltf2interface": {+      "version": "9.5.0",+      "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-9.5.0.tgz",+      "integrity": "sha512-kgq1xWYZSqIyZ+qTG9deXAnVjiDg3wMZHXm4zqzcFZclSmJDV2ACGMEh5C5Styzg59VS5XeukdYGjmBcJmCebA==",+      "peer": true+    },+    "node_modules/balanced-match": {+      "version": "1.0.2",+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",+      "dev": true+    },+    "node_modules/binary-extensions": {+      "version": "2.3.0",+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",+      "dev": true,+      "engines": {+        "node": ">=8"+      },+      "funding": {+        "url": "https://github.com/sponsors/sindresorhus"+      }+    },+    "node_modules/brace-expansion": {+      "version": "1.1.14",+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",+      "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",+      "dev": true,+      "dependencies": {+        "balanced-match": "^1.0.0",+        "concat-map": "0.0.1"+      }+    },+    "node_modules/braces": {+      "version": "3.0.3",+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",+      "dev": true,+      "dependencies": {+        "fill-range": "^7.1.1"+      },+      "engines": {+        "node": ">=8"+      }+    },+    "node_modules/buffer-crc32": {+      "version": "1.0.0",+      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",+      "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",+      "dev": true,+      "engines": {+        "node": ">=8.0.0"+      }+    },+    "node_modules/buffer-from": {+      "version": "1.1.2",+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",+      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",+      "dev": true+    },+    "node_modules/chokidar": {+      "version": "3.6.0",+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",+      "dev": true,+      "dependencies": {+        "anymatch": "~3.1.2",+        "braces": "~3.0.2",+        "glob-parent": "~5.1.2",+        "is-binary-path": "~2.1.0",+        "is-glob": "~4.0.1",+        "normalize-path": "~3.0.0",+        "readdirp": "~3.6.0"+      },+      "engines": {+        "node": ">= 8.10.0"+      },+      "funding": {+        "url": "https://paulmillr.com/funding/"+      },+      "optionalDependencies": {+        "fsevents": "~2.3.2"+      }+    },+    "node_modules/code-red": {+      "version": "1.0.4",+      "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",+      "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==",+      "dependencies": {+        "@jridgewell/sourcemap-codec": "^1.4.15",+        "@types/estree": "^1.0.1",+        "acorn": "^8.10.0",+        "estree-walker": "^3.0.3",+        "periscopic": "^3.1.0"+      }+    },+    "node_modules/commander": {+      "version": "2.20.3",+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",+      "dev": true+    },+    "node_modules/concat-map": {+      "version": "0.0.1",+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",+      "dev": true+    },+    "node_modules/css-tree": {+      "version": "2.3.1",+      "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",+      "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",+      "dependencies": {+        "mdn-data": "2.0.30",+        "source-map-js": "^1.0.1"+      },+      "engines": {+        "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"+      }+    },+    "node_modules/csstype": {+      "version": "3.2.3",+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",+      "peer": true+    },+    "node_modules/debug": {+      "version": "4.4.3",+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",+      "dev": true,+      "dependencies": {+        "ms": "^2.1.3"+      },+      "engines": {+        "node": ">=6.0"+      },+      "peerDependenciesMeta": {+        "supports-color": {+          "optional": true+        }+      }+    },+    "node_modules/deepmerge": {+      "version": "4.3.1",+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",+      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",+      "dev": true,+      "engines": {+        "node": ">=0.10.0"+      }+    },+    "node_modules/detect-indent": {+      "version": "6.1.0",+      "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",+      "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==",+      "dev": true,+      "engines": {+        "node": ">=8"+      }+    },+    "node_modules/embla-carousel": {+      "version": "8.6.0",+      "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",+      "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",+      "peer": true+    },+    "node_modules/embla-carousel-autoplay": {+      "version": "8.6.0",+      "resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.6.0.tgz",+      "integrity": "sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==",+      "peer": true,+      "peerDependencies": {+        "embla-carousel": "8.6.0"+      }+    },+    "node_modules/embla-carousel-fade": {+      "version": "8.6.0",+      "resolved": "https://registry.npmjs.org/embla-carousel-fade/-/embla-carousel-fade-8.6.0.tgz",+      "integrity": "sha512-qaYsx5mwCz72ZrjlsXgs1nKejSrW+UhkbOMwLgfRT7w2LtdEB03nPRI06GHuHv5ac2USvbEiX2/nAHctcDwvpg==",+      "peer": true,+      "peerDependencies": {+        "embla-carousel": "8.6.0"+      }+    },+    "node_modules/es6-promise": {+      "version": "3.3.1",+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",+      "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==",+      "dev": true+    },+    "node_modules/esbuild": {+      "version": "0.21.5",+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",+      "dev": true,+      "hasInstallScript": true,+      "bin": {+        "esbuild": "bin/esbuild"+      },+      "engines": {+        "node": ">=12"+      },+      "optionalDependencies": {+        "@esbuild/aix-ppc64": "0.21.5",+        "@esbuild/android-arm": "0.21.5",+        "@esbuild/android-arm64": "0.21.5",+        "@esbuild/android-x64": "0.21.5",+        "@esbuild/darwin-arm64": "0.21.5",+        "@esbuild/darwin-x64": "0.21.5",+        "@esbuild/freebsd-arm64": "0.21.5",+        "@esbuild/freebsd-x64": "0.21.5",+        "@esbuild/linux-arm": "0.21.5",+        "@esbuild/linux-arm64": "0.21.5",+        "@esbuild/linux-ia32": "0.21.5",+        "@esbuild/linux-loong64": "0.21.5",+        "@esbuild/linux-mips64el": "0.21.5",+        "@esbuild/linux-ppc64": "0.21.5",+        "@esbuild/linux-riscv64": "0.21.5",+        "@esbuild/linux-s390x": "0.21.5",+        "@esbuild/linux-x64": "0.21.5",+        "@esbuild/netbsd-x64": "0.21.5",+        "@esbuild/openbsd-x64": "0.21.5",+        "@esbuild/sunos-x64": "0.21.5",+        "@esbuild/win32-arm64": "0.21.5",+        "@esbuild/win32-ia32": "0.21.5",+        "@esbuild/win32-x64": "0.21.5"+      }+    },+    "node_modules/estree-walker": {+      "version": "3.0.3",+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",+      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",+      "dependencies": {+        "@types/estree": "^1.0.0"+      }+    },+    "node_modules/fill-range": {+      "version": "7.1.1",+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",+      "dev": true,+      "dependencies": {+        "to-regex-range": "^5.0.1"+      },+      "engines": {+        "node": ">=8"+      }+    },+    "node_modules/fs.realpath": {+      "version": "1.0.0",+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",+      "dev": true+    },+    "node_modules/fsevents": {+      "version": "2.3.3",+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",+      "dev": true,+      "hasInstallScript": true,+      "optional": true,+      "os": [+        "darwin"+      ],+      "engines": {+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"+      }+    },+    "node_modules/glob": {+      "version": "7.2.3",+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",+      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting [email protected]",+      "dev": true,+      "dependencies": {+        "fs.realpath": "^1.0.0",+        "inflight": "^1.0.4",+        "inherits": "2",+        "minimatch": "^3.1.1",+        "once": "^1.3.0",+        "path-is-absolute": "^1.0.0"+      },+      "engines": {+        "node": "*"+      },+      "funding": {+        "url": "https://github.com/sponsors/isaacs"+      }+    },+    "node_modules/glob-parent": {+      "version": "5.1.2",+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",+      "dev": true,+      "dependencies": {+        "is-glob": "^4.0.1"+      },+      "engines": {+        "node": ">= 6"+      }+    },+    "node_modules/graceful-fs": {+      "version": "4.2.11",+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",+      "dev": true+    },+    "node_modules/inflight": {+      "version": "1.0.6",+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",+      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",+      "dev": true,+      "dependencies": {+        "once": "^1.3.0",+        "wrappy": "1"+      }+    },+    "node_modules/inherits": {+      "version": "2.0.4",+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",+      "dev": true+    },+    "node_modules/is-binary-path": {+      "version": "2.1.0",+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",+      "dev": true,+      "dependencies": {+        "binary-extensions": "^2.0.0"+      },+      "engines": {+        "node": ">=8"+      }+    },+    "node_modules/is-extglob": {+      "version": "2.1.1",+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",+      "dev": true,+      "engines": {+        "node": ">=0.10.0"+      }+    },+    "node_modules/is-glob": {+      "version": "4.0.3",+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",+      "dev": true,+      "dependencies": {+        "is-extglob": "^2.1.1"+      },+      "engines": {+        "node": ">=0.10.0"+      }+    },+    "node_modules/is-number": {+      "version": "7.0.0",+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",+      "dev": true,+      "engines": {+        "node": ">=0.12.0"+      }+    },+    "node_modules/is-reference": {+      "version": "3.0.3",+      "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",+      "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",+      "dependencies": {+        "@types/estree": "^1.0.6"+      }+    },+    "node_modules/keyborg": {+      "version": "2.14.1",+      "resolved": "https://registry.npmjs.org/keyborg/-/keyborg-2.14.1.tgz",+      "integrity": "sha512-/WmmVBa6Me3hIKAOIyIq1sql+6oydQZzGMBDLNfOcJ8710byMsq3KSLS8GQhBJHOMtvnXnUBrDAIbABcZVipcg==",+      "peer": true+    },+    "node_modules/kleur": {+      "version": "4.1.5",+      "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",+      "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",+      "dev": true,+      "engines": {+        "node": ">=6"+      }+    },+    "node_modules/locate-character": {+      "version": "3.0.0",+      "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",+      "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="+    },+    "node_modules/lodash.debounce": {+      "version": "4.0.8",+      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",+      "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",+      "peer": true+    },+    "node_modules/magic-string": {+      "version": "0.30.21",+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",+      "dependencies": {+        "@jridgewell/sourcemap-codec": "^1.5.5"+      }+    },+    "node_modules/mdn-data": {+      "version": "2.0.30",+      "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",+      "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="+    },+    "node_modules/min-indent": {+      "version": "1.0.1",+      "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",+      "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",+      "dev": true,+      "engines": {+        "node": ">=4"+      }+    },+    "node_modules/minimatch": {+      "version": "3.1.5",+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",+      "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",+      "dev": true,+      "dependencies": {+        "brace-expansion": "^1.1.7"+      },+      "engines": {+        "node": "*"+      }+    },+    "node_modules/minimist": {+      "version": "1.2.8",+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",+      "dev": true,+      "funding": {+        "url": "https://github.com/sponsors/ljharb"+      }+    },+    "node_modules/mkdirp": {+      "version": "0.5.6",+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",+      "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",+      "dev": true,+      "dependencies": {+        "minimist": "^1.2.6"+      },+      "bin": {+        "mkdirp": "bin/cmd.js"+      }+    },+    "node_modules/mri": {+      "version": "1.2.0",+      "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",+      "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",+      "dev": true,+      "engines": {+        "node": ">=4"+      }+    },+    "node_modules/ms": {+      "version": "2.1.3",+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",+      "dev": true+    },+    "node_modules/nanoid": {+      "version": "3.3.12",+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",+      "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",+      "dev": true,+      "funding": [+        {+          "type": "github",+          "url": "https://github.com/sponsors/ai"+        }+      ],+      "bin": {+        "nanoid": "bin/nanoid.cjs"+      },+      "engines": {+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"+      }+    },+    "node_modules/normalize-path": {+      "version": "3.0.0",+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",+      "dev": true,+      "engines": {+        "node": ">=0.10.0"+      }+    },+    "node_modules/once": {+      "version": "1.4.0",+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",+      "dev": true,+      "dependencies": {+        "wrappy": "1"+      }+    },+    "node_modules/path-is-absolute": {+      "version": "1.0.1",+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",+      "dev": true,+      "engines": {+        "node": ">=0.10.0"+      }+    },+    "node_modules/periscopic": {+      "version": "3.1.0",+      "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",+      "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==",+      "dependencies": {+        "@types/estree": "^1.0.0",+        "estree-walker": "^3.0.0",+        "is-reference": "^3.0.0"+      }+    },+    "node_modules/picocolors": {+      "version": "1.1.1",+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",+      "dev": true+    },+    "node_modules/picomatch": {+      "version": "2.3.2",+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",+      "dev": true,+      "engines": {+        "node": ">=8.6"+      },+      "funding": {+        "url": "https://github.com/sponsors/jonschlinkert"+      }+    },+    "node_modules/postcss": {+      "version": "8.5.13",+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz",+      "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==",+      "dev": true,+      "funding": [+        {+          "type": "opencollective",+          "url": "https://opencollective.com/postcss/"+        },+        {+          "type": "tidelift",+          "url": "https://tidelift.com/funding/github/npm/postcss"+        },+        {+          "type": "github",+          "url": "https://github.com/sponsors/ai"+        }+      ],+      "dependencies": {+        "nanoid": "^3.3.11",+        "picocolors": "^1.1.1",+        "source-map-js": "^1.2.1"+      },+      "engines": {+        "node": "^10 || ^12 || >=14"+      }+    },+    "node_modules/react": {+      "version": "19.2.5",+      "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",+      "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",+      "peer": true,+      "engines": {+        "node": ">=0.10.0"+      }+    },+    "node_modules/react-dom": {+      "version": "19.2.5",+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",+      "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",+      "peer": true,+      "dependencies": {+        "scheduler": "^0.27.0"+      },+      "peerDependencies": {+        "react": "^19.2.5"+      }+    },+    "node_modules/readdirp": {+      "version": "3.6.0",+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",+      "dev": true,+      "dependencies": {+        "picomatch": "^2.2.1"+      },+      "engines": {+        "node": ">=8.10.0"+      }+    },+    "node_modules/rimraf": {+      "version": "2.7.1",+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",+      "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",+      "deprecated": "Rimraf versions prior to v4 are no longer supported",+      "dev": true,+      "dependencies": {+        "glob": "^7.1.3"+      },+      "bin": {+        "rimraf": "bin.js"+      }+    },+    "node_modules/rollup": {+      "version": "4.60.2",+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",+      "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",+      "dev": true,+      "dependencies": {+        "@types/estree": "1.0.8"+      },+      "bin": {+        "rollup": "dist/bin/rollup"+      },+      "engines": {+        "node": ">=18.0.0",+        "npm": ">=8.0.0"+      },+      "optionalDependencies": {+        "@rollup/rollup-android-arm-eabi": "4.60.2",+        "@rollup/rollup-android-arm64": "4.60.2",+        "@rollup/rollup-darwin-arm64": "4.60.2",+        "@rollup/rollup-darwin-x64": "4.60.2",+        "@rollup/rollup-freebsd-arm64": "4.60.2",+        "@rollup/rollup-freebsd-x64": "4.60.2",+        "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",+        "@rollup/rollup-linux-arm-musleabihf": "4.60.2",+        "@rollup/rollup-linux-arm64-gnu": "4.60.2",+        "@rollup/rollup-linux-arm64-musl": "4.60.2",+        "@rollup/rollup-linux-loong64-gnu": "4.60.2",+        "@rollup/rollup-linux-loong64-musl": "4.60.2",+        "@rollup/rollup-linux-ppc64-gnu": "4.60.2",+        "@rollup/rollup-linux-ppc64-musl": "4.60.2",+        "@rollup/rollup-linux-riscv64-gnu": "4.60.2",+        "@rollup/rollup-linux-riscv64-musl": "4.60.2",+        "@rollup/rollup-linux-s390x-gnu": "4.60.2",+        "@rollup/rollup-linux-x64-gnu": "4.60.2",+        "@rollup/rollup-linux-x64-musl": "4.60.2",+        "@rollup/rollup-openbsd-x64": "4.60.2",+        "@rollup/rollup-openharmony-arm64": "4.60.2",+        "@rollup/rollup-win32-arm64-msvc": "4.60.2",+        "@rollup/rollup-win32-ia32-msvc": "4.60.2",+        "@rollup/rollup-win32-x64-gnu": "4.60.2",+        "@rollup/rollup-win32-x64-msvc": "4.60.2",+        "fsevents": "~2.3.2"+      }+    },+    "node_modules/rtl-css-js": {+      "version": "1.16.1",+      "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz",+      "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==",+      "peer": true,+      "dependencies": {+        "@babel/runtime": "^7.1.2"+      }+    },+    "node_modules/sade": {+      "version": "1.8.1",+      "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",+      "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",+      "dev": true,+      "dependencies": {+        "mri": "^1.1.0"+      },+      "engines": {+        "node": ">=6"+      }+    },+    "node_modules/sander": {+      "version": "0.5.1",+      "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz",+      "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==",+      "dev": true,+      "dependencies": {+        "es6-promise": "^3.1.2",+        "graceful-fs": "^4.1.3",+        "mkdirp": "^0.5.1",+        "rimraf": "^2.5.2"+      }+    },+    "node_modules/scheduler": {+      "version": "0.27.0",+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",+      "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",+      "peer": true+    },+    "node_modules/sorcery": {+      "version": "0.11.1",+      "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.1.tgz",+      "integrity": "sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==",+      "dev": true,+      "dependencies": {+        "@jridgewell/sourcemap-codec": "^1.4.14",+        "buffer-crc32": "^1.0.0",+        "minimist": "^1.2.0",+        "sander": "^0.5.0"+      },+      "bin": {+        "sorcery": "bin/sorcery"+      }+    },+    "node_modules/source-map": {+      "version": "0.6.1",+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",+      "dev": true,+      "engines": {+        "node": ">=0.10.0"+      }+    },+    "node_modules/source-map-js": {+      "version": "1.2.1",+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",+      "engines": {+        "node": ">=0.10.0"+      }+    },+    "node_modules/source-map-support": {+      "version": "0.5.21",+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",+      "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",+      "dev": true,+      "dependencies": {+        "buffer-from": "^1.0.0",+        "source-map": "^0.6.0"+      }+    },+    "node_modules/strip-indent": {+      "version": "3.0.0",+      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",+      "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",+      "dev": true,+      "dependencies": {+        "min-indent": "^1.0.0"+      },+      "engines": {+        "node": ">=8"+      }+    },+    "node_modules/stylis": {+      "version": "4.4.0",+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",+      "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",+      "peer": true+    },+    "node_modules/svelte": {+      "version": "4.2.20",+      "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.20.tgz",+      "integrity": "sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==",+      "dependencies": {+        "@ampproject/remapping": "^2.2.1",+        "@jridgewell/sourcemap-codec": "^1.4.15",+        "@jridgewell/trace-mapping": "^0.3.18",+        "@types/estree": "^1.0.1",+        "acorn": "^8.9.0",+        "aria-query": "^5.3.0",+        "axobject-query": "^4.0.0",+        "code-red": "^1.0.3",+        "css-tree": "^2.3.1",+        "estree-walker": "^3.0.3",+        "is-reference": "^3.0.1",+        "locate-character": "^3.0.0",+        "magic-string": "^0.30.4",+        "periscopic": "^3.1.0"+      },+      "engines": {+        "node": ">=16"+      }+    },+    "node_modules/svelte-check": {+      "version": "3.8.6",+      "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.6.tgz",+      "integrity": "sha512-ij0u4Lw/sOTREP13BdWZjiXD/BlHE6/e2e34XzmVmsp5IN4kVa3PWP65NM32JAgwjZlwBg/+JtiNV1MM8khu0Q==",+      "dev": true,+      "dependencies": {+        "@jridgewell/trace-mapping": "^0.3.17",+        "chokidar": "^3.4.1",+        "picocolors": "^1.0.0",+        "sade": "^1.7.4",+        "svelte-preprocess": "^5.1.3",+        "typescript": "^5.0.3"+      },+      "bin": {+        "svelte-check": "bin/svelte-check"+      },+      "peerDependencies": {+        "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0"+      }+    },+    "node_modules/svelte-hmr": {+      "version": "0.16.0",+      "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz",+      "integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==",+      "dev": true,+      "engines": {+        "node": "^12.20 || ^14.13.1 || >= 16"+      },+      "peerDependencies": {+        "svelte": "^3.19.0 || ^4.0.0"+      }+    },+    "node_modules/svelte-preprocess": {+      "version": "5.1.4",+      "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz",+      "integrity": "sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==",+      "dev": true,+      "hasInstallScript": true,+      "dependencies": {+        "@types/pug": "^2.0.6",+        "detect-indent": "^6.1.0",+        "magic-string": "^0.30.5",+        "sorcery": "^0.11.0",+        "strip-indent": "^3.0.0"+      },+      "engines": {+        "node": ">= 16.0.0"+      },+      "peerDependencies": {+        "@babel/core": "^7.10.2",+        "coffeescript": "^2.5.1",+        "less": "^3.11.3 || ^4.0.0",+        "postcss": "^7 || ^8",+        "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",+        "pug": "^3.0.0",+        "sass": "^1.26.8",+        "stylus": "^0.55.0",+        "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0",+        "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0",+        "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0"+      },+      "peerDependenciesMeta": {+        "@babel/core": {+          "optional": true+        },+        "coffeescript": {+          "optional": true+        },+        "less": {+          "optional": true+        },+        "postcss": {+          "optional": true+        },+        "postcss-load-config": {+          "optional": true+        },+        "pug": {+          "optional": true+        },+        "sass": {+          "optional": true+        },+        "stylus": {+          "optional": true+        },+        "sugarss": {+          "optional": true+        },+        "typescript": {+          "optional": true+        }+      }+    },+    "node_modules/tabster": {+      "version": "8.8.0",+      "resolved": "https://registry.npmjs.org/tabster/-/tabster-8.8.0.tgz",+      "integrity": "sha512-eGFXgtvKOQP5BywDI9Ngs+Atm6TRj45epAAqWKyVoi+HmOmdamEB//1H/FttLdNly/+Cz+GJ4RN8TnXTw0KwfA==",+      "peer": true,+      "dependencies": {+        "keyborg": "^2.14.0",+        "tslib": "^2.8.1"+      }+    },+    "node_modules/terser": {+      "version": "5.46.2",+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz",+      "integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==",+      "dev": true,+      "dependencies": {+        "@jridgewell/source-map": "^0.3.3",+        "acorn": "^8.15.0",+        "commander": "^2.20.0",+        "source-map-support": "~0.5.20"+      },+      "bin": {+        "terser": "bin/terser"+      },+      "engines": {+        "node": ">=10"+      }+    },+    "node_modules/to-regex-range": {+      "version": "5.0.1",+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",+      "dev": true,+      "dependencies": {+        "is-number": "^7.0.0"+      },+      "engines": {+        "node": ">=8.0"+      }+    },+    "node_modules/tslib": {+      "version": "2.8.1",+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",+      "peer": true+    },+    "node_modules/typescript": {+      "version": "5.9.3",+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",+      "dev": true,+      "bin": {+        "tsc": "bin/tsc",+        "tsserver": "bin/tsserver"+      },+      "engines": {+        "node": ">=14.17"+      }+    },+    "node_modules/use-sync-external-store": {+      "version": "1.6.0",+      "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",+      "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",+      "peer": true,+      "peerDependencies": {+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"+      }+    },+    "node_modules/usehooks-ts": {+      "version": "3.1.1",+      "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz",+      "integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==",+      "peer": true,+      "dependencies": {+        "lodash.debounce": "^4.0.8"+      },+      "engines": {+        "node": ">=16.15.0"+      },+      "peerDependencies": {+        "react": "^16.8.0  || ^17 || ^18 || ^19 || ^19.0.0-rc"+      }+    },+    "node_modules/vite": {+      "version": "5.4.21",+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",+      "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",+      "dev": true,+      "dependencies": {+        "esbuild": "^0.21.3",+        "postcss": "^8.4.43",+        "rollup": "^4.20.0"+      },+      "bin": {+        "vite": "bin/vite.js"+      },+      "engines": {+        "node": "^18.0.0 || >=20.0.0"+      },+      "funding": {+        "url": "https://github.com/vitejs/vite?sponsor=1"+      },+      "optionalDependencies": {+        "fsevents": "~2.3.3"+      },+      "peerDependencies": {+        "@types/node": "^18.0.0 || >=20.0.0",+        "less": "*",+        "lightningcss": "^1.21.0",+        "sass": "*",+        "sass-embedded": "*",+        "stylus": "*",+        "sugarss": "*",+        "terser": "^5.4.0"+      },+      "peerDependenciesMeta": {+        "@types/node": {+          "optional": true+        },+        "less": {+          "optional": true+        },+        "lightningcss": {+          "optional": true+        },+        "sass": {+          "optional": true+        },+        "sass-embedded": {+          "optional": true+        },+        "stylus": {+          "optional": true+        },+        "sugarss": {+          "optional": true+        },+        "terser": {+          "optional": true+        }+      }+    },+    "node_modules/vitefu": {+      "version": "0.2.5",+      "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",+      "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==",+      "dev": true,+      "peerDependencies": {+        "vite": "^3.0.0 || ^4.0.0 || ^5.0.0"+      },+      "peerDependenciesMeta": {+        "vite": {+          "optional": true+        }+      }+    },+    "node_modules/wrappy": {+      "version": "1.0.2",+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",+      "dev": true+    }+  }+}
vendor/adom-step/vendor/adom-3d-viewer-babylon9/package.jsonadded+43
@@ -0,0 +1,43 @@+{+  "name": "adom-3d-viewer-babylon9",+  "version": "0.2.0",+  "description": "Fork of adom-inc/3d-viewer upgraded to Babylon 9.5 with the redesigned Inspector + 3d-viewer-design-skill-compliant defaults (background gradient, bottom-light toggle, medium-gray ground plane, world-origin axes).",+  "type": "module",+  "main": "./dist/index.js",+  "module": "./dist/index.js",+  "types": "./dist/index.d.ts",+  "exports": {+    ".": {+      "import": "./dist/index.js",+      "types": "./dist/index.d.ts"+    }+  },+  "files": [+    "dist",+    "dist-standalone",+    "src",+    "standalone.ts"+  ],+  "scripts": {+    "build": "vite build",+    "build:standalone": "vite build --config vite.standalone.config.ts",+    "dev": "vite build --watch",+    "prepare": "npm run build"+  },+  "dependencies": {+    "@babylonjs/core": "^9.5.0",+    "@babylonjs/inspector": "^9.5.0",+    "@babylonjs/loaders": "^9.5.0",+    "svelte": "^4.0.0 || ^5.0.0"+  },+  "devDependencies": {+    "@sveltejs/vite-plugin-svelte": "^3.1.2",+    "svelte-check": "^3.8.6",+    "terser": "^5.46.2",+    "typescript": "^5.5.4",+    "vite": "^5.4.1"+  },+  "peerDependencies": {+    "svelte": "^4.0.0 || ^5.0.0"+  }+}
vendor/adom-step/vendor/adom-3d-viewer-babylon9/publish.shadded+187
@@ -0,0 +1,187 @@+#!/usr/bin/env bash+# adom-3d-viewer-babylon9 release publisher.+# Wiki-first: ships an ESM bundle as a tarball at+# wiki-<host>/static/apps/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.tar.gz.+# Consumer tools (chiplinter, future Adom 3D apps) curl + tar -xz into their+# vendor/3d-viewer/ at install time.+#+# Idempotent: safe to re-run for the same version.++set -euo pipefail+cd "$(dirname "$0")"++VERSION=$(cat VERSION | tr -d '[:space:]')+if [ -z "$VERSION" ]; then+  echo "ERROR: VERSION file is empty" >&2; exit 1+fi++if [ -t 1 ]; then C_OK='\033[32m'; C_ERR='\033[31m'; C_STEP='\033[36m'; C_RESET='\033[0m'+else C_OK=''; C_ERR=''; C_STEP=''; C_RESET=''+fi+ok()   { printf '%sOK:%s %s\n' "$C_OK" "$C_RESET" "$1"; }+fail() { printf '%sERROR:%s %s\n' "$C_ERR" "$C_RESET" "$1" >&2; exit 1; }+step() { printf '\n%s== %s ==%s\n' "$C_STEP" "$1" "$C_RESET"; }++# ── 0. Sanity: VERSION == package.json == wiki/page.json version ──+step "0. version sanity"+PKG_VERSION=$(jq -r '.version' package.json)+if [ "$VERSION" != "$PKG_VERSION" ]; then+  fail "VERSION ($VERSION) != package.json ($PKG_VERSION). Update one or the other."+fi+WIKI_VERSION=$(jq -r '.version' wiki/page.json)+if [ "$VERSION" != "$WIKI_VERSION" ]; then+  fail "VERSION ($VERSION) != wiki/page.json ($WIKI_VERSION). Update wiki/page.json: { version, metadata.releases.adom_docker.binary_url }"+fi+ok "VERSION + package.json + wiki/page.json all at $VERSION"++# ── 1. Build the standalone ESM bundle ─────────────────────────────+step "1. vite build --config vite.standalone.config.ts"+if [ ! -d node_modules ]; then+  npm install+fi+npx vite build --config vite.standalone.config.ts+if [ ! -f dist-standalone/adom-3d-viewer-babylon9.esm.js ]; then+  fail "build did not produce dist-standalone/adom-3d-viewer-babylon9.esm.js"+fi+echo "$VERSION" > dist-standalone/VERSION+ok "built dist-standalone/ ($(du -sh dist-standalone | awk '{print $1}'))"++# ── 2. Pack the tarball ────────────────────────────────────────────+step "2. tar dist-standalone -> /tmp/adom-3d-viewer-babylon9.tar.gz"+TAR=/tmp/adom-3d-viewer-babylon9.tar.gz+rm -f "$TAR"+tar -czf "$TAR" -C dist-standalone .+ok "tarball $(ls -la "$TAR" | awk '{print $5}') bytes"++# ── 3. Optional: git tag ───────────────────────────────────────────+step "3. git tag (optional)"+if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && git remote get-url origin >/dev/null 2>&1; then+  git add -A+  if ! git diff --cached --quiet; then+    git commit -m "release v$VERSION"+    ok "committed"+  else+    ok "nothing to commit"+  fi+  git tag -f "v$VERSION"+  if git push origin main 2>/dev/null && git push origin "v$VERSION" --force 2>/dev/null; then+    ok "pushed main + v$VERSION tag"+  else+    ok "skipped git push (no auth or offline)"+  fi+else+  ok "no git remote configured; skipping"+fi++# ── 4. Build a real changelog ──────────────────────────────────────+build_changelog() {+  if [ -n "${CHANGELOG:-}" ]; then+    printf '%s' "$CHANGELOG"+    return+  fi+  if [ -f CHANGELOG.md ]; then+    local section+    section=$(awk -v v="v$VERSION" '+      /^## / { if (in_section) exit; if ($0 ~ "## "v"($| )") { in_section=1; next } }+      in_section { print }+    ' CHANGELOG.md | sed '/^[[:space:]]*$/d' | head -20)+    if [ -n "$section" ]; then+      printf 'v%s:\n%s' "$VERSION" "$section"+      return+    fi+  fi+  if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then+    local prev_tag log_lines log_range+    prev_tag=$(git tag --sort=-v:refname 2>/dev/null | grep -v "^v$VERSION$" | head -1)+    if [ -n "$prev_tag" ]; then+      log_range="$prev_tag..HEAD"+    else+      log_range="HEAD~10..HEAD"+    fi+    log_lines=$(git log "$log_range" --oneline --no-merges 2>/dev/null \+      | grep -viE '^\w+\s+(wip|merge|typo|fmt|fix typo)\b' \+      | sed 's/^\w\+ /- /' \+      | head -15)+    if [ -n "$log_lines" ]; then+      printf 'v%s — changes since %s:\n%s' "$VERSION" "${prev_tag:-HEAD~10}" "$log_lines"+      return+    fi+  fi+  printf 'v%s release of adom-3d-viewer-babylon9 — Babylon 9.5 ESM bundle with redesigned Inspector v9 for Adom 3D tools to vendor.' "$VERSION"+}++# ── 5. Wiki page publish ───────────────────────────────────────────+step "5. adom-wiki page publish apps/adom-3d-viewer-babylon9"+TITLE=$(jq -r '.title' wiki/page.json)+BRIEF=$(jq -r '.brief' wiki/page.json)+CHANGELOG_TEXT=$(build_changelog)+echo "── changelog ──"+printf '%s\n' "$CHANGELOG_TEXT" | head -20+echo "──────────────"++# Pick a hero image — prefer a hand-curated wiki/hero.png if present, else+# fall back to the most-recent pup screenshot.+HERO=""+HERO_FROM_PUP=$(ls -t /tmp/conduit-screenshots/screenshot-*.png 2>/dev/null | head -1 || true)+if [ -f wiki/hero.png ]; then+  HERO=wiki/hero.png+elif [ -n "$HERO_FROM_PUP" ]; then+  HERO=$HERO_FROM_PUP+fi+HERO_FLAGS=()+if [ -n "$HERO" ]; then+  HERO_FLAGS+=(--hero-image "$HERO" --hero-caption "adom-3d-viewer-babylon9 in chiplinter's inspector — Babylon 9.5 rendering a chip with FP overlays + scene-graph debug HUD.")+else+  HERO_FLAGS+=(--force-no-hero)+fi++adom-wiki page publish apps/adom-3d-viewer-babylon9 \+  --title "$TITLE" \+  --brief "$BRIEF" \+  --body-md wiki/body.md \+  --version "$VERSION" \+  --changelog "$CHANGELOG_TEXT" \+  "${HERO_FLAGS[@]}" \+  --sample-prompt "Drop into my tool|install adom-3d-viewer-babylon9 and symlink it into my tool's vendor/3d-viewer/ directory" \+  --sample-prompt "Show babylon 9 inspector|launch a quick page with this viewer + the inspector open so I can poke at the scene graph" \+  --sample-prompt "Upgrade chiplinter viewer|chiplinter's vendored bundle is stale, refresh it from this wiki page" \+  --sample-prompt "Build a new 3D app|I'm starting a new Adom 3D tool, scaffold it on top of adom-3d-viewer-babylon9" \+  --sample-prompt "Convert kicad GLB|load a kicad-cli GLB into the viewer and call rotateYUpToZUp so axes line up with adom Z-up convention"+ok "wiki page published apps/adom-3d-viewer-babylon9 v$VERSION"++# ── 6. Wiki asset upload (tarball, stable filename) ────────────────+step "6. adom-wiki asset upload"+adom-wiki asset upload apps/adom-3d-viewer-babylon9 \+  --asset-type docker-binary \+  --file "$TAR" \+  --caption "adom-3d-viewer-babylon9 v$VERSION — Babylon 9.5 ESM bundle. Stable URL: /static/apps/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.tar.gz" \+  --changelog "v$VERSION tarball upload — Babylon 9.5 + Inspector v9 ESM build (vite standalone)."+ok "uploaded tarball"++# ── 7. Push metadata ───────────────────────────────────────────────+step "7. adom-wiki page edit --field metadata"+TMP_META=$(mktemp)+jq --arg v "$VERSION" '.metadata | .version = $v' wiki/page.json > "$TMP_META"+adom-wiki page edit apps/adom-3d-viewer-babylon9 --field metadata --body-md "$TMP_META" \+  --changelog "v$VERSION metadata: discovery_triggers + install_hint + binary_url refresh"+rm -f "$TMP_META"+ok "metadata updated"++# ── 8. Verify wiki tarball serves the right VERSION ────────────────+step "8. verify wiki tarball"+BINARY_URL=$(jq -r '.metadata.releases.adom_docker.binary_url' wiki/page.json)+CHECK_DIR=$(mktemp -d)+if ! curl -fsSL "$BINARY_URL" -o "$CHECK_DIR/bundle.tar.gz"; then+  rm -rf "$CHECK_DIR"+  fail "wiki tarball download failed at $BINARY_URL (may need a minute; re-run step 8)"+fi+tar -xzf "$CHECK_DIR/bundle.tar.gz" -C "$CHECK_DIR"+WIKI_VER=$(cat "$CHECK_DIR/VERSION" 2>/dev/null | tr -d '[:space:]' || echo "MISSING")+rm -rf "$CHECK_DIR"+if [ "$WIKI_VER" != "$VERSION" ]; then+  fail "wiki bundle VERSION ($WIKI_VER) != VERSION ($VERSION) — propagation lag, retry in 30s"+fi+ok "wiki tarball at $BINARY_URL ships VERSION=$VERSION"++step "DONE — adom-3d-viewer-babylon9 v$VERSION published"+echo "Auto-discover consumers (chiplinter etc.) will pick this up on their next install."
vendor/adom-step/vendor/adom-3d-viewer-babylon9/src/ThreeDViewer.svelteadded+688
@@ -0,0 +1,688 @@+<script lang="ts">+    import { onMount, onDestroy } from 'svelte';+    import { SceneLoader } from '@babylonjs/core/Loading/sceneLoader';+    import '@babylonjs/loaders/glTF';+    import { Vector3 } from '@babylonjs/core/Maths/math.vector';+    import { Color3 } from '@babylonjs/core/Maths/math.color';+    import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';+    import type { Mesh } from '@babylonjs/core/Meshes/mesh';+    import { AdomStatics } from './lib/adom-babylon/AdomStatics';+    import { SceneBuilder } from './lib/adom-babylon/3DViewer/SceneBuilder';+    import * as AxisHelpers from './lib/adom-babylon/3DViewer/AxisHelpers';+    import { ResizableLoadingScreen } from './lib/adom-babylon/ui/ResizableLoadingScreen';+    import type { ViewCubeUICallbacks } from './lib/adom-babylon/3DViewer/ViewCubeBuilder';+    import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';+    import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';+    import { type Scene, type Engine, type Camera, type ShadowGenerator, PBRMetallicRoughnessMaterial } from '@babylonjs/core';++    export let zUp: boolean = true;+    export let modelUrl: string | undefined = undefined;+    export let showViewCube: boolean = true;+    export let showGround: boolean = false;+    export let environmentUrl: string | undefined = undefined;+    /** Initial camera projection. Applied once when the viewer is ready. Use setProjectionMode() to change at runtime. */+    export let initialViewMode: 'perspective' | 'orthographic' = 'perspective';++    let container: HTMLDivElement;+    let canvas: HTMLCanvasElement;+    let resizeObserver: ResizeObserver | null = null;+    let loadedRootNodes: TransformNode[] = [];+    /** User-added roots (e.g. native Babylon meshes) included in framing. */+    let contentRoots: TransformNode[] = [];+    let groundMesh: Mesh | null = null;+    let isOrtho: boolean = false;++    // Tooltip / dropdown state driven by ViewCube callbacks+    let tooltipText: string | null = null;+    let tooltipX = 0;+    let tooltipY = 0;+    let dropdownOpen = false;+    let dropdownX = 0;+    let dropdownY = 0;+    let dropdownOpenedAt = 0;++    const viewCubeCallbacks: ViewCubeUICallbacks = {+        onTooltip(text, x, y) {+            tooltipText = text;+            tooltipX = x;+            tooltipY = y;+        },+        onHomeClick() {+            goHome();+        },+        onFitClick() {+            frameModel();+        },+        onViewModeClick(x, y) {+            tooltipText = null;+            dropdownX = x;+            dropdownY = y;+            dropdownOpen = !dropdownOpen;+            if (dropdownOpen) dropdownOpenedAt = Date.now();+        }+    };++    function selectProjection(ortho: boolean) {+        setProjectionMode(ortho);+        dropdownOpen = false;+    }++    function onKeydown(e: KeyboardEvent) {+        if (e.key === 'Escape' && dropdownOpen) {+            dropdownOpen = false;+        }+    }++    function onWindowClick(e: MouseEvent) {+        // Skip clicks that happen within 300ms of opening (the same+        // pointerdown that opened the dropdown also fires a click)+        if (dropdownOpen && Date.now() - dropdownOpenedAt > 300) {+            const target = e.target as HTMLElement;+            if (!target.closest('.vcube-dropdown')) {+                dropdownOpen = false;+            }+        }+    }++    async function initViewer() {+        if (!canvas || !container) return;+        await AdomStatics.init(canvas, zUp);+        const engine = AdomStatics.engine!;+        const scene = AdomStatics.scene!;++        canvas.width = container.clientWidth;+        canvas.height = container.clientHeight;++        const loadingScreen = new ResizableLoadingScreen(canvas, 'Loading...', '#bfbfbf');+        engine.loadingScreen = loadingScreen;+        engine.displayLoadingUI();++        await SceneBuilder.init(engine, { zUp, showViewCube, environmentUrl, viewCubeCallbacks });++        scene.executeWhenReady(async () => {+            engine.runRenderLoop(() => scene.render());++            const cam = AdomStatics.camera;+            if (cam && initialViewMode === 'orthographic' && !cam.isOrthographic()) {+                cam.toggleCameraMode();+                isOrtho = true;+            } else if (cam) {+                isOrtho = cam.isOrthographic();+            }++            if (modelUrl) {+                await loadModel(modelUrl);+            }+            engine.hideLoadingUI();++            canvas.addEventListener('dblclick', onDoubleClick);+        });++        resizeObserver = new ResizeObserver(() => {+            const eng = AdomStatics.engine;+            const sc = AdomStatics.scene;+            if (canvas && eng) {+                const cont = canvas.parentElement;+                if (cont) {+                    canvas.width = cont.clientWidth;+                    canvas.height = cont.clientHeight;+                    eng.resize();+                    sc?.render();+                }+            }+        });+        resizeObserver.observe(container);+    }++    export async function loadModel(url: string): Promise<void> {+        const scene = AdomStatics.scene;+        if (!scene) return;+        const shadowGenerator = AdomStatics.shadowGenerator;++        const lastSlash = url.lastIndexOf('/');+        const rootUrl = lastSlash >= 0 ? url.substring(0, lastSlash + 1) : '';+        const sceneFilename = lastSlash >= 0 ? url.substring(lastSlash + 1) : url;++        // Detect file type for Babylon.js plugin selection.+        // URLs without a file extension (e.g. /api/export/glb?name=...)+        // need an explicit pluginExtension hint so Babylon.js uses the right loader.+        let pluginExt: string | undefined;+        const cleanPath = url.split('?')[0].split('#')[0];+        if (!cleanPath.match(/\.(glb|gltf|obj|stl|ply|splat)$/i)) {+            // Default to GLB for extensionless URLs (most common in our ecosystem)+            pluginExt = '.glb';+        }++        const result = await SceneLoader.ImportMeshAsync('', rootUrl, sceneFilename, scene, undefined, pluginExt);+        const root = result.meshes[0];+        if (root) {+            loadedRootNodes.push(root);+            result.meshes.forEach((m: AbstractMesh) => {+                m.receiveShadows = true;+                if (shadowGenerator && m.getTotalVertices && m.getTotalVertices() > 0) {+                    shadowGenerator.addShadowCaster(m);+                }+            });+            AdomStatics.refreshShadows();+            frameModel();+        }+    }++    export function clearScene(): void {+        loadedRootNodes.forEach((node) => node.dispose());+        loadedRootNodes = [];+        AdomStatics.refreshShadows();+        AdomStatics.camera?.disableBounds();+    }++    /** Babylon scene — use to create/modify meshes (e.g. MeshBuilder.CreateSphere). */+    export function getScene(): Scene | null {+        return AdomStatics.scene;+    }++    /** Babylon engine (resize, etc.). */+    export function getEngine(): Engine | null {+        return AdomStatics.engine;+    }++    /** Active camera (orbit/pan/zoom). */+    export function getCamera(): Camera | null {+        return AdomStatics.camera ?? null;+    }++    /** Shadow generator — call addShadowCaster(mesh) for custom meshes that should cast shadows. */+    export function getShadowGenerator(): ShadowGenerator | null {+        return AdomStatics.shadowGenerator;+    }++    /**+     * Register a root (e.g. parent of spheres) so it is included in frameModel() and optional shadow setup.+     * Does not dispose the node on clearScene(); remove with removeContentRoot or dispose yourself.+     */+    export function addContentRoot(node: TransformNode, options?: { castShadows?: boolean; receiveShadows?: boolean }): void {+        if (contentRoots.includes(node)) return;+        contentRoots.push(node);+        const sg = AdomStatics.shadowGenerator;+        const cast = options?.castShadows !== false;+        const receive = options?.receiveShadows !== false;+        const visit = (m: AbstractMesh) => {+            if (receive) m.receiveShadows = true;+            if (cast && sg && m.getTotalVertices?.() && m.getTotalVertices() > 0) sg.addShadowCaster(m);+        };+        if ((node as AbstractMesh).getTotalVertices?.() && (node as AbstractMesh).getTotalVertices()! > 0) {+            visit(node as AbstractMesh);+        }+        if ('getChildMeshes' in node) {+            (node as AbstractMesh).getChildMeshes(false).forEach(visit);+        }+        AdomStatics.refreshShadows();+    }++    /** Unregister a content root from framing (does not dispose the node). */+    export function removeContentRoot(node: TransformNode): void {+        const i = contentRoots.indexOf(node);+        if (i >= 0) contentRoots.splice(i, 1);+        AdomStatics.refreshShadows();+    }++    /** Show or hide the ground plane at runtime. Re-runs frameModel to create/remove it. */+    export function setGroundVisible(visible: boolean): void {+        showGround = visible;+        if (loadedRootNodes.length > 0 || contentRoots.length > 0) {+            frameModel();+        } else if (!visible && groundMesh) {+            groundMesh.dispose();+            groundMesh = null;+        }+    }++    /** Toggle the bottom-fill HemisphericLight. Off by default — flip on+     *  when inspecting BGA balls / QFN pads / through-hole leg bottoms.+     *  Intensity 0.6 when on (matches the top hemisphere's perceived+     *  brightness once spotlight contribution is added). */+    export function setBottomLight(on: boolean): void {+        const bl = AdomStatics.bottomLight;+        if (bl) bl.intensity = on ? 0.6 : 0;+    }++    /** Returns the current bottom-light state (true == on). */+    export function getBottomLightState(): boolean {+        const bl = AdomStatics.bottomLight;+        return !!(bl && bl.intensity > 0);+    }++    /** Jump camera to the home view (front-top-right, like the view cube corner) and re-frame the model. No animation. */+    export function goHome(): void {+        const cam = AdomStatics.camera;+        if (!cam) return;+        cam.rotateToCubeTopFrontRight(0);+        frameModel();+    }++    /**+     * 3d-viewer-design §8 axis-helper API.+     *   target: 'world' | 'mesh-local' — corner triad is always on (no toggle)+     *   enabled: undefined → toggle current state; bool → set explicitly+     * Returns the new state.+     */+    export function toggleAxes(target: 'world' | 'mesh-local', enabled?: boolean): boolean {+        const scene = AdomStatics.scene;+        if (!scene) return false;+        if (target === 'world') {+            const next = enabled !== undefined ? enabled : !AxisHelpers.isWorldAxesVisible(scene);+            AxisHelpers.setWorldAxesVisible(scene, next);+            return next;+        }+        if (target === 'mesh-local') {+            const next = enabled !== undefined ? enabled : !AxisHelpers.isMeshLocalAxesVisible(scene);+            AxisHelpers.setMeshLocalAxesVisible(scene, next);+            // Auto-attach a local-axes helper to every loaded root that+            // doesn't already have one (safe for §8b lazy-init).+            for (const root of loadedRootNodes) {+                AxisHelpers.addMeshLocalAxes(scene, root);+            }+            return next;+        }+        return false;+    }+    export function getAxesState(): { world: boolean; meshLocal: boolean } {+        const scene = AdomStatics.scene;+        if (!scene) return { world: false, meshLocal: false };+        return {+            world: AxisHelpers.isWorldAxesVisible(scene),+            meshLocal: AxisHelpers.isMeshLocalAxesVisible(scene),+        };+    }++    /** Set camera projection to perspective or orthographic. */+    export function setProjectionMode(ortho: boolean): void {+        const cam = AdomStatics.camera;+        if (!cam) return;+        const currentlyOrtho = cam.isOrthographic();+        if (currentlyOrtho !== ortho) {+            cam.toggleCameraMode();+            isOrtho = cam.isOrthographic();+        }+    }++    /** Current projection: `'orthographic'` or `'perspective'`. */+    export function getProjectionMode(): 'perspective' | 'orthographic' {+        return AdomStatics.camera?.isOrthographic() ? 'orthographic' : 'perspective';+    }++    const SKYBOX_SIZE = 5000;++    /**+     * Double-click handler: pick the mesh, compute its bounds,+     * calculate optimal camera distance, and animate to it via moveCamTo.+     */+    function onDoubleClick(): void {+        const scene = AdomStatics.scene;+        const camera = AdomStatics.camera;+        if (!scene || !camera) return;++        const pickInfo = scene.pick(scene.pointerX, scene.pointerY);+        if (!pickInfo?.hit || !pickInfo.pickedMesh) return;++        const mesh = pickInfo.pickedMesh;++        // Only zoom to meshes that belong to a known model or content root+        const allRoots = new Set<TransformNode>([...loadedRootNodes, ...contentRoots]);+        const loadedSet = new Set<TransformNode>(loadedRootNodes);+        let root: TransformNode | null = null;+        let current: TransformNode | null = mesh;+        while (current) {+            if (allRoots.has(current)) { root = current; break; }+            current = (current.parent as TransformNode | null) ?? null;+        }+        if (!root) return;++        // For GLB models: zoom to the __root__ node (visual meshes).+        // For content roots (native shapes): zoom to the individual clicked mesh.+        let targetNode: TransformNode;+        if (loadedSet.has(root)) {+            const rootChild = root.getChildren().find(c => c.name === '__root__');+            targetNode = (rootChild as TransformNode) ?? root;+        } else {+            targetNode = mesh;+        }++        // Collect visible meshes under the target+        const meshes: AbstractMesh[] = [];+        if ('getChildMeshes' in targetNode) {+            meshes.push(...(targetNode as AbstractMesh).getChildMeshes(false).filter(+                m => m.isVisible && m.getTotalVertices && m.getTotalVertices() > 0+            ));+        }+        if ((targetNode as AbstractMesh).getTotalVertices?.() > 0) {+            meshes.push(targetNode as AbstractMesh);+        }+        if (meshes.length === 0) return;++        // Compute world bounds+        let worldMin = new Vector3(Infinity, Infinity, Infinity);+        let worldMax = new Vector3(-Infinity, -Infinity, -Infinity);+        for (const m of meshes) {+            m.computeWorldMatrix(true);+            const bb = m.getBoundingInfo().boundingBox;+            worldMin = Vector3.Minimize(worldMin, bb.minimumWorld);+            worldMax = Vector3.Maximize(worldMax, bb.maximumWorld);+        }+        if (!isFinite(worldMin.x)) return;++        const center = Vector3.Center(worldMin, worldMax);+        const sizeVec = worldMax.subtract(worldMin);+        const diag = sizeVec.length();+        if (diag === 0) return;++        // Calculate optimal camera distance to fill ~80% of the viewport+        const fov = camera.fov || 0.8;+        const aspect = (AdomStatics.engine?.getAspectRatio(camera) ?? 1);+        const hFov = 2 * Math.atan(Math.tan(fov / 2) * aspect);+        const effectiveFov = Math.min(fov, hFov);+        const radius = (diag / 2) / Math.tan(effectiveFov / 2) / 0.8;++        // Animate to the target using moveCamTo (keeps current alpha/beta)+        const pos = camera.position.clone();+        camera.moveCamTo(+            pos.x, pos.y, pos.z,+            center.x, center.y, center.z,+            radius,+            camera.alpha,+            camera.beta,+            60, 50+        );+    }++    /**+     * Frame the camera so the given roots (or all loaded/content roots) fill the viewport.+     * Adapts clip planes, zoom limits, interaction speeds, light frustum, and ground to the model's scale.+     * @param fillFraction Multiplier on zoomOn radius (1 = fills viewport, >1 = more zoomed out). Default 1.+     * @param rootsToFrame When provided, frame only these roots. Otherwise frame all.+     */+    export function frameModel(fillFraction: number = 1, rootsToFrame?: TransformNode[]): void {+        const scene = AdomStatics.scene;+        const camera = AdomStatics.camera;+        const roots = rootsToFrame ?? [...loadedRootNodes, ...contentRoots];+        if (!scene || !camera || roots.length === 0) return;++        const meshes: AbstractMesh[] = [];+        for (const root of roots) {+            if ('getChildMeshes' in root) {+                meshes.push(...(root as AbstractMesh).getChildMeshes(false));+            }+            if ((root as AbstractMesh).getTotalVertices?.() > 0) {+                meshes.push(root as AbstractMesh);+            }+        }+        const visible = meshes.filter(m =>+            m.isVisible && m.getTotalVertices && m.getTotalVertices() > 0+        );+        if (visible.length === 0) return;++        let worldMin = new Vector3(Infinity, Infinity, Infinity);+        let worldMax = new Vector3(-Infinity, -Infinity, -Infinity);+        for (const m of visible) {+            m.computeWorldMatrix(true);+            const bi = m.getBoundingInfo();+            worldMin = Vector3.Minimize(worldMin, bi.boundingBox.minimumWorld);+            worldMax = Vector3.Maximize(worldMax, bi.boundingBox.maximumWorld);+        }+        if (!isFinite(worldMin.x)) return;++        const extent = worldMax.subtract(worldMin);+        const modelSize = extent.length();+        if (modelSize === 0) return;++        camera.lowerRadiusLimit = 0;+        camera.upperRadiusLimit = Infinity;++        camera.zoomOn(visible, true);+        camera.radius *= fillFraction;++        const radius = camera.radius;+        const halfSize = modelSize / 2;+        const center = Vector3.Center(worldMin, worldMax);++        camera.minZ = Math.max(halfSize * 0.01, 0.01);+        camera.maxZ = Math.max(radius * 20, SKYBOX_SIZE * 1.5);+        camera.panningSensibility = 5000 / halfSize;++        const pad = 0.2;+        camera.setTargetBounds(+            worldMin.x - extent.x * pad, worldMax.x + extent.x * pad,+            worldMin.y - extent.y * pad, worldMax.y + extent.y * pad,+            worldMin.z - extent.z * pad, worldMax.z + extent.z * pad+        );+        camera.upperRadiusLimit = radius / 0.3;+        camera.lowerRadiusLimit = halfSize * 0.01;++        // 3d-viewer-design §8a — world-origin axis helper sized to 15% of+        // the current scene radius (modelSize/2). Recomputed on every+        // frameModel so it scales with the user's framing.+        AxisHelpers.refreshAxisScale(scene, halfSize);++        // Reposition SpotLight proportional to model size, angled from front-left.+        const spotLight = AdomStatics.spotLight;+        if (spotLight) {+            const spotHeight = modelSize * 3;+            const lateralOffset = modelSize * 1.2;+            if (zUp) {+                spotLight.position = new Vector3(+                    center.x + lateralOffset,+                    center.y + lateralOffset ,+                    center.z + spotHeight+                );+            } else {+                spotLight.position = new Vector3(+                    center.x + lateralOffset,+                    center.y + spotHeight,+                    center.z - lateralOffset * 0.5+                );+            }+            spotLight.direction = center.subtract(spotLight.position).normalize();+            // Scale intensity with the square of distance (inverse-square falloff).+            // Hydrogen reference: 2M intensity at height 625 for ~200-unit models.+            const hydrogenRef = 625;+            const scale = (spotHeight / hydrogenRef);+            spotLight.intensity = 2_000_000 * scale * scale;+            spotLight.shadowMinZ = spotHeight * 0.15;+            spotLight.shadowMaxZ = spotHeight * 3.2;+        }++        if (showGround) {+            const groundSize = modelSize * 4;+            if (groundMesh) groundMesh.dispose();+            groundMesh = MeshBuilder.CreateGround('shadowGround', { width: groundSize, height: groundSize }, scene);+            groundMesh.receiveShadows = true;+            const mat = new PBRMetallicRoughnessMaterial('groundMat', scene);+            mat.baseColor = new Color3(0.15, 0.15, 0.15);+            mat.metallic = 0.0;+            mat.roughness = 0.8;+            mat.backFaceCulling = false;+            groundMesh.material = mat;+            if (zUp) {+                groundMesh.rotation.x = Math.PI / 2;+                groundMesh.position = new Vector3(center.x, center.y, worldMin.z);+            } else {+                groundMesh.position = new Vector3(center.x, worldMin.y, center.z);+            }+        } else if (groundMesh) {+            groundMesh.dispose();+            groundMesh = null;+        }+        AdomStatics.refreshShadows();+    }++    /** Show the Babylon Inspector. Lazy-imported so it lands in its own+     * module scope (avoids "object is not extensible" frozen-namespace+     * errors that happen with static import in our ESM build).+     *+     * Also suppresses Babylon Inspector v9's "first-time-user" teaching+     * popovers — they pop up next to every toolbar button (Extensions,+     * Translate, Rotate, Scale, etc.) on every page load with no+     * persistent dismissal state. Adom users (and other expert users)+     * have seen them many times and don't need re-onboarding each time+     * they open the inspector. We zero them out via a global CSS rule+     * the first time showDebugLayer fires; the rule covers Fluent UI's+     * TeachingPopoverSurface class, which is what the inspector uses. */+    function suppressInspectorTeachingPopovers(): void {+        if (document.getElementById('__adom_kill_teaching_popovers__')) return;+        const style = document.createElement('style');+        style.id = '__adom_kill_teaching_popovers__';+        style.textContent =+            // Fluent UI v9 TeachingPopover surface — the actual onboarding bubble.+            '.fui-TeachingPopoverSurface{display:none !important;}\n' ++            // Defensive: also hide any teaching-popover trigger badge that may+            // glow / pulse to draw attention to the toolbar button itself.+            '.fui-TeachingPopover__indicator{display:none !important;}\n';+        document.head.appendChild(style);+    }++    export async function showDebugLayer(): Promise<void> {+        const scene = AdomStatics.scene;+        if (!scene) return;+        await import('@babylonjs/inspector');+        suppressInspectorTeachingPopovers();+        await scene.debugLayer.show({ embedMode: true, overlay: true });+    }++    /** Hide the Inspector. */+    export function hideDebugLayer(): void {+        AdomStatics.scene?.debugLayer.hide();+    }++    /** Toggle the Inspector visibility. */+    export async function toggleDebugLayer(): Promise<void> {+        const scene = AdomStatics.scene;+        if (!scene) return;+        if (scene.debugLayer.isVisible()) {+            hideDebugLayer();+        } else {+            await showDebugLayer();+        }+    }++    onMount(() => {+        initViewer();+    });++    onDestroy(() => {+        canvas?.removeEventListener('dblclick', onDoubleClick);+        resizeObserver?.disconnect();+        AdomStatics.engine?.stopRenderLoop();+        AdomStatics.dispose();+    });+</script>++<svelte:window on:keydown={onKeydown} on:click={onWindowClick} />++<div class="viewer-container" bind:this={container}>+    <canvas bind:this={canvas}></canvas>++    {#if tooltipText && !dropdownOpen}+        <div+            class="vcube-tooltip"+            style="left: {tooltipX - 12}px; top: {tooltipY + 12}px;"+        >+            {tooltipText}+        </div>+    {/if}++    {#if dropdownOpen}+        <div+            class="vcube-dropdown"+            style="left: {dropdownX - 12}px; top: {dropdownY + 12}px;"+        >+            <button+                class="vcube-dropdown-item"+                class:active={!isOrtho}+                on:click|stopPropagation={() => selectProjection(false)}+            >+                <span class="vcube-check">{!isOrtho ? '✓' : ''}</span>+                Perspective+            </button>+            <button+                class="vcube-dropdown-item"+                class:active={isOrtho}+                on:click|stopPropagation={() => selectProjection(true)}+            >+                <span class="vcube-check">{isOrtho ? '✓' : ''}</span>+                Orthographic+            </button>+        </div>+    {/if}+</div>++<style>+    .viewer-container {+        position: relative;+        width: 100%;+        height: 100%;+        overflow: hidden;+    }+    .viewer-container canvas {+        display: block;+        width: 100%;+        height: 100%;+        outline: none;+    }++    /* ViewCube tooltip */+    .vcube-tooltip {+        position: absolute;+        pointer-events: none;+        transform: translateX(-100%);+        background: rgba(30, 34, 38, 0.92);+        color: #e0e0e0;+        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;+        font-size: 12px;+        line-height: 1;+        padding: 5px 10px;+        border-radius: 4px;+        white-space: nowrap;+        z-index: 20;+    }++    /* ViewCube dropdown */+    .vcube-dropdown {+        position: absolute;+        transform: translateX(-100%);+        background: rgba(40, 44, 48, 0.96);+        border: 1px solid rgba(255, 255, 255, 0.12);+        border-radius: 6px;+        padding: 4px 0;+        z-index: 30;+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);+        min-width: 140px;+    }++    .vcube-dropdown-item {+        display: flex;+        align-items: center;+        width: 100%;+        padding: 6px 12px;+        border: none;+        background: none;+        color: #d0d0d0;+        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;+        font-size: 13px;+        cursor: pointer;+        text-align: left;+        gap: 6px;+    }++    .vcube-dropdown-item:hover {+        background: rgba(255, 255, 255, 0.08);+    }++    .vcube-dropdown-item.active {+        color: #00b8b0;+    }++    .vcube-check {+        display: inline-block;+        width: 14px;+        font-size: 13px;+    }+</style>
vendor/adom-step/vendor/adom-3d-viewer-babylon9/src/VIEWER_CONTEXT.mdadded+82
@@ -0,0 +1,82 @@+# 3D Viewer: Context for AIs and maintainers++This document explains how the viewer works, especially its **Z-up vs Y-up** behavior and how it differs from a typical Babylon.js app. Use it when modifying the viewer or when another AI needs to reason about the codebase.++---++## Coordinate system: Z-up vs Y-up (and why it matters)++The viewer can run in two orientations: **Z-up** (default) or **Y-up**. That choice affects the camera, lights, ground plane, and how imported models appear. It exists because different tools and formats use different conventions.++### How Babylon.js normally works++- **Babylon.js is Y-up by default.** The default camera `upVector` is `(0, 1, 0)`, the default ground plane lies in the XZ plane (normal = Y), and many examples and docs assume Y is "up."+- Many other ecosystems (CAD, robotics, some GLTF exporters) use **Z-up**: Z is "up," and the "floor" is in the XY plane.++If you load a Z-up GLB into a default Babylon scene without adjusting anything, the model will look like it's lying on its side: what the model considers "up" (Z) is aligned with Babylon's Y axis instead of Z.++### What this viewer does when `zUp={true}` (default)++- **Scene:** `scene.useRightHandedSystem = true` is set in `AdomStatics.init`. In this codebase, handedness is tied to the `zUp` flag.+- **Camera:** In `AdomCamera`'s constructor, `camera.upVector = (0, 0, 1)` when `zUp` is true, so the orbit camera treats Z as up. Orbit, pan, zoom, and Q/E "up/down" all respect Z as vertical.+- **Lights:** In `SceneBuilder.init`, the hemispheric light direction and the SpotLight's initial position are chosen based on `zUp`. In `ThreeDViewer.svelte`'s `frameModel()`, the SpotLight is repositioned: when `zUp` is true, height is applied along Z and lateral offset in X/Y; when false, height is along Y and offset in X/Z. So "above the model" and "to the side" stay correct for both conventions.+- **Ground plane (optional):** Babylon's `MeshBuilder.CreateGround` creates a quad in the **XZ** plane (normal Y). When `zUp` is true, the viewer rotates that ground by `Math.PI/2` around X so the plane lies in the **XY** plane (normal Z) and positions it at `worldMin.z`. When `zUp` is false, the ground stays in the XZ plane and is positioned at `worldMin.y`. So in both modes the ground sits "under" the content along the chosen up axis.+- **View cube:** The view cube is rendered in a **separate** mini-scene in `ViewCubeBuilder.ts`, and that scene is **always Z-up**: `camera2.upVector = new Vector3(0, 0, 1)`. The cube's labels (TOP, FRONT, etc.) and the "Home" (top-front-right) view therefore assume Z is up. If the main scene is run with `zUp={false}` (Y-up), the view cube still shows a Z-up orientation, which can be visually inconsistent with the main scene.++So with **`zUp={true}`** (default), the main scene (camera, lights, ground) is consistent with Z as the vertical axis. With **`zUp={false}`**, the main scene matches traditional Babylon Y-up; only the view cube remains Z-up.++### Source-specific conventions: Fusion (Z-up) vs KiCad (Y-up)++- **Autodesk Fusion 360** typically exports GLB/GLTF with **Z-up**: the design "up" is the Z axis. Using this viewer with **`zUp={true}`** (default) shows the model upright without extra transforms.+- **KiCad** (and many PCB / electronics tools) often use **Y-up** in their 3D export or internal coordinates. For such models, **`zUp={false}`** may match the intended orientation so the board doesn't appear rotated or "lying down."++If a model appears wrong side up, try flipping the `zUp` prop and reloading. The viewer does **not** auto-detect up axis from the file.++### Summary table++| `zUp`   | Camera up | Ground plane       | Typical use case               |+|---------|-----------|--------------------|--------------------------------|+| `true`  | Z         | XY plane, at min Z  | Fusion-style, CAD, Z-up GLBs   |+| `false` | Y         | XZ plane, at min Y  | Babylon default, KiCad, Y-up   |++---++## Quirks and differences from a typical Babylon viewer++These are the main ways this component diverges from a vanilla Babylon.js setup.++1. **Single SpotLight for shadows (no DirectionalLight)**  +   The main scene uses a **SpotLight** as the only shadow-casting light (and for the lit "pool" on the ground). There is no DirectionalLight. The shadow generator is attached to the SpotLight. Light position and direction are updated in `frameModel()` proportionally to the model size. This matches a reference "Hydrogen" app and keeps the ground from being uniformly lit (the cone naturally darkens the edges).++2. **SpotLight position and intensity scale with the model**  +   In `ThreeDViewer.svelte`'s `frameModel()`, the SpotLight is moved to a height and lateral offset derived from the model's bounding size (e.g. `spotHeight = modelSize * 3`). Intensity is scaled by the square of that height (inverse-square falloff) so illumination stays consistent across very different model scales. So lighting is **model-relative**, not fixed in world space.++3. **Ground plane is optional and model-relative**  +   When `showGround` is true, the ground is created **inside** `frameModel()`: its size and position are based on the current content bounds. There is no permanent "floor" at world zero; it's recreated when you frame and is tied to the content's extent and to `zUp`.++4. **View cube is always Z-up**  +   The view cube lives in its own scene (`ViewCubeBuilder`) with a fixed Z-up camera. The "Home" button and cube corners (e.g. top-front-right) assume Z is up. If the main scene is Y-up, the cube's orientation does not match the main scene's "up."++5. **Camera bounds and limits are set in `frameModel()`**  +   Radius limits, target bounds, panning sensibility, and near/far clip planes are recomputed when you call `frameModel()`. They are driven by the current set of loaded roots and `addContentRoot` roots, so the camera behavior is content-aware rather than fixed.++6. **High-precision depth**  +   The engine is created with `useHighPrecisionFloats: true` to reduce z-fighting when zoomed out or with large scenes.++7. **No automatic up-axis detection**  +   The viewer does not read the GLB/GLTF or detect whether the asset is Z-up or Y-up. The app (or user) must set the `zUp` prop to match the source (e.g. Fusion vs KiCad).++8. **Bundled environment and loading assets**  +   A default studio `.env` and loading logo are embedded as base64 so the package works without serving extra static files. The `environmentUrl` prop can override the environment for PBR reflections.++---++## Where `zUp` is used in the codebase++- **`AdomStatics.init(canvas, zUp)`** — Sets `scene.useRightHandedSystem = zUp` and passes `zUp` into `AdomCamera`.+- **`AdomCamera` constructor** — Sets `this.upVector = zUp ? new Vector3(0, 0, 1) : new Vector3(0, 1, 0)`.+- **`SceneBuilder.init(engine, { zUp, ... })`** — Hemispheric light direction and SpotLight initial position/direction use `zUp`.+- **`ThreeDViewer.svelte`** — `frameModel()` uses `zUp` for SpotLight position (which axis is "height" vs "lateral") and for ground plane rotation and position (`worldMin.z` vs `worldMin.y`).+- **`ViewCubeBuilder`** — Does **not** take `zUp`; the view cube scene is always Z-up.++This should give other AIs enough context to understand how Z-up works and why the viewer is structured this way.
vendor/adom-step/vendor/adom-3d-viewer-babylon9/src/index.tsadded+1
@@ -0,0 +1 @@+export { default as ThreeDViewer } from './ThreeDViewer.svelte';
vendor/adom-step/vendor/adom-3d-viewer-babylon9/src/lib/adom-babylon/3DViewer/AxisHelpers.tsadded+162
@@ -0,0 +1,162 @@+// Axis helpers — 3d-viewer-design skill §8 (non-negotiable).+//+// Three layers, all attached at scene-init time:+//   §8a world-origin axis helper   (default ON, toggleable)+//   §8b mesh-local axis helper      (default OFF, toggleable, parented to root)+//   §8c screen-space corner triad   (always on, no toggle, viewport overlay)+//+// R/G/B for X/Y/Z — never brand-tinted. Lengths scale to scene extent so+// they neither dominate small chip previews nor disappear in big assemblies.++import { AxesViewer } from '@babylonjs/core/Debug/axesViewer';+import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera';+import { Vector3 } from '@babylonjs/core/Maths/math.vector';+import { Viewport } from '@babylonjs/core/Maths/math.viewport';+import type { Scene } from '@babylonjs/core/scene';+import type { Camera } from '@babylonjs/core/Cameras/camera';+import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';+import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';++// Per-scene state stored on the scene object so consumers can grab it via+// scene.metadata.adomAxes.+export interface AdomAxesState {+    world: AxesViewer | null;+    meshLocal: Map<TransformNode | AbstractMesh, AxesViewer>;+    meshLocalEnabled: boolean;+    cornerTriad: { cam: ArcRotateCamera; axes: AxesViewer } | null;+    sceneExtent: number;+}++const STATE_KEY = '__adomAxes';++/** Get (or create) the per-scene axis state. */+function getState(scene: Scene): AdomAxesState {+    const meta = scene.metadata || (scene.metadata = {});+    if (!meta[STATE_KEY]) {+        meta[STATE_KEY] = {+            world: null,+            meshLocal: new Map(),+            meshLocalEnabled: false,+            cornerTriad: null,+            sceneExtent: 1,+        } as AdomAxesState;+    }+    return meta[STATE_KEY] as AdomAxesState;+}++/**+ * Initialize §8a + §8c at scene boot. Call once from SceneBuilder.+ * §8b helpers are added per-loaded-mesh via addMeshLocalAxes().+ */+export function initAxisHelpers(scene: Scene, mainCamera: Camera, opts?: { worldDefaultOn?: boolean }): void {+    const state = getState(scene);+    const worldOn = opts?.worldDefaultOn !== false;++    // §8a — world-origin helper. Length = 15% of scene extent (refreshed+    // by frameModel via refreshScale()). Initial guess of 1m; consumers+    // call refreshScale(scene, sceneRadius) after frameModel completes.+    const worldAxes = new AxesViewer(scene, 0.15);+    state.world = worldAxes;+    setEnabled(worldAxes, worldOn);+    // AxesViewer creates X/Y/Z meshes with .isPickable = true by default;+    // disable so they don't intercept Shift+Alt+Click etc.+    [worldAxes.xAxis, worldAxes.yAxis, worldAxes.zAxis].forEach(n => {+        n.getChildMeshes().forEach(m => { m.isPickable = false; });+        if ((n as AbstractMesh).isPickable !== undefined) (n as AbstractMesh).isPickable = false;+    });++    // §8c — screen-space corner triad: NOT YET IMPLEMENTED.+    // Tried the second-camera-viewport approach (multi-camera in+    // scene.activeCameras with a 10% bottom-left viewport on a triad+    // camera); the viewport is correctly set and the meshes carry the+    // right layerMask, but the viewer's runRenderLoop only renders the+    // primary scene.activeCamera and skips the activeCameras array.+    // Likely needs a UtilityLayerRenderer with explicit shouldRender ++    // an attached secondary engine.render() pass. Tracked as a v0.3.0+    // follow-up; for now §8a + §8b cover the bulk of "where is origin"+    // debugging value.+    state.cornerTriad = null;+}++/** Refresh world-axes scale to 15% of current scene radius (§8a). Call+ *  after frameModel(). */+export function refreshAxisScale(scene: Scene, sceneRadius: number): void {+    const state = getState(scene);+    state.sceneExtent = sceneRadius;+    const worldLen = Math.max(sceneRadius * 0.15, 0.001);+    if (state.world) {+        // AxesViewer doesn't expose a setSize after construction, so we+        // dispose + recreate when scale changes meaningfully (>10%).+        const old = state.world;+        const enabled = isEnabled(old);+        old.dispose();+        const fresh = new AxesViewer(scene, worldLen);+        [fresh.xAxis, fresh.yAxis, fresh.zAxis].forEach(n => {+            n.getChildMeshes().forEach(m => { m.isPickable = false; });+        });+        setEnabled(fresh, enabled);+        state.world = fresh;+    }+}++/** §8a — toggle visibility of the world-origin helper. */+export function setWorldAxesVisible(scene: Scene, visible: boolean): void {+    const state = getState(scene);+    if (state.world) setEnabled(state.world, visible);+}++export function isWorldAxesVisible(scene: Scene): boolean {+    const state = getState(scene);+    return state.world ? isEnabled(state.world) : false;+}++/**+ * §8b — attach a local-origin helper to a loaded root node and return it.+ * Helper is parented to `root` so it follows the mesh's transform.+ * Visibility tracks the global mesh-local toggle.+ */+export function addMeshLocalAxes(scene: Scene, root: TransformNode | AbstractMesh): AxesViewer {+    const state = getState(scene);+    if (state.meshLocal.has(root)) return state.meshLocal.get(root)!;+    // Sized to the root's bounding sphere if it has one, else fall back to+    // 40% of scene extent.+    let r = 0;+    if ((root as AbstractMesh).getBoundingInfo) {+        try { r = (root as AbstractMesh).getBoundingInfo().boundingSphere.radius; }+        catch { /* TransformNode without bounds */ }+    }+    if (!r) r = state.sceneExtent;+    const axes = new AxesViewer(scene, r * 0.4);+    [axes.xAxis, axes.yAxis, axes.zAxis].forEach(n => {+        n.parent = root;+        n.getChildMeshes().forEach(m => { m.isPickable = false; });+    });+    setEnabled(axes, state.meshLocalEnabled);+    state.meshLocal.set(root, axes);+    return axes;+}++/** §8b — toggle visibility of every mesh-local helper. */+export function setMeshLocalAxesVisible(scene: Scene, visible: boolean): void {+    const state = getState(scene);+    state.meshLocalEnabled = visible;+    for (const axes of state.meshLocal.values()) setEnabled(axes, visible);+}++export function isMeshLocalAxesVisible(scene: Scene): boolean {+    return getState(scene).meshLocalEnabled;+}++/** Drop axis helpers attached to a no-longer-present root. */+export function removeMeshLocalAxes(scene: Scene, root: TransformNode | AbstractMesh): void {+    const state = getState(scene);+    const axes = state.meshLocal.get(root);+    if (axes) { axes.dispose(); state.meshLocal.delete(root); }+}++function setEnabled(axes: AxesViewer, on: boolean): void {+    [axes.xAxis, axes.yAxis, axes.zAxis].forEach(n => n.setEnabled(on));+}+function isEnabled(axes: AxesViewer): boolean {+    return axes.xAxis.isEnabled();+}
vendor/adom-step/vendor/adom-3d-viewer-babylon9/src/lib/adom-babylon/3DViewer/SceneBuilder.tsadded+151
@@ -0,0 +1,151 @@+import { TransformNode } from '@babylonjs/core/Meshes/transformNode';+import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight';+import { SpotLight } from '@babylonjs/core/Lights/spotLight';+import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator';+import { CubeTexture } from '@babylonjs/core/Materials/Textures/cubeTexture';+import { Vector3 } from '@babylonjs/core/Maths/math.vector';+import { Color3, Color4 } from '@babylonjs/core/Maths/math.color';+import type { Engine, Scene } from '@babylonjs/core';+import { ViewCubeBuilder, type ViewCubeUICallbacks } from './ViewCubeBuilder';+import { AdomStatics } from '../AdomStatics';+import { STUDIO_ENV_BASE64 } from '../assets/studioEnv';+import { initAxisHelpers } from './AxisHelpers';++export interface SceneBuilderOptions {+    zUp: boolean;+    showViewCube: boolean;+    environmentUrl?: string;+    viewCubeCallbacks?: ViewCubeUICallbacks;+}++export class SceneBuilder {+    static init(engine: Engine, options: SceneBuilderOptions = { zUp: true, showViewCube: true }): Promise<void> {+        if (!AdomStatics.scene) {+            console.error("SceneBuilder Error: AdomStatics.scene is null.");+            return Promise.reject(new Error("Scene is undefined in AdomStatics."));+        }+        const scene = AdomStatics.scene;+        const { zUp, showViewCube, environmentUrl, viewCubeCallbacks } = options;++        return new Promise<void>((resolve, reject) => {+            try {+                const rootNode = new TransformNode('ViewerRoot', scene);++                const hemDir = zUp ? new Vector3(0, 0, 1) : new Vector3(0, 1, 0);+                const hemisphericLight = new HemisphericLight('light', hemDir, scene);+                hemisphericLight.intensity = 0.2;+                hemisphericLight.parent = rootNode;+                hemisphericLight.diffuse = new Color3(1, 1, 1);+                hemisphericLight.groundColor = new Color3(0.65, 0.65, 0.65);++                // Bottom fill light — illuminates the underside of parts so a+                // user inspecting BGA balls / QFN pads / through-hole leg+                // bottoms can see them clearly. Default OFF (intensity 0)+                // because top-down lighting + ambient is the cleaner default+                // for the typical "look at chip from above" use case;+                // toggled on via ThreeDViewer.setBottomLight() when the user+                // wants to inspect the bottom face. The direction vector+                // points UP (toward +Z in Z-up, +Y in Y-up) so the upper+                // hemisphere of the light wraps around the model from below.+                const bottomDir = zUp ? new Vector3(0, 0, -1) : new Vector3(0, -1, 0);+                const bottomLight = new HemisphericLight('bottomLight', bottomDir, scene);+                bottomLight.intensity = 0;  // toggled to ~0.6 by setBottomLight(true)+                bottomLight.parent = rootNode;+                bottomLight.diffuse = new Color3(1, 1, 1);+                bottomLight.groundColor = new Color3(0.5, 0.5, 0.5);+                AdomStatics.bottomLight = bottomLight;++                // SpotLight — sole direct light, matching Hydrogen.+                // Cone naturally darkens ground edges. Repositioned by+                // frameModel() proportionally to the loaded model's size.+                const spotPos = zUp+                    ? new Vector3(600, -300, 1500)+                    : new Vector3(600, 1500, -300);+                const spotDir = new Vector3(0, 0, 0).subtract(spotPos).normalize();+                const spotLight = new SpotLight('spotLight', spotPos, spotDir, Math.PI * 0.7, 50, scene);+                spotLight.intensity = 2_000_000;+                spotLight.parent = rootNode;+                AdomStatics.spotLight = spotLight;++                const shadowGenerator = new ShadowGenerator(8192, spotLight);+                scene.shadowsEnabled = true;+                shadowGenerator.usePercentageCloserFiltering = true;+                shadowGenerator.filteringQuality = ShadowGenerator.QUALITY_HIGH;+                shadowGenerator.bias = 0.0002 / 1000;+                shadowGenerator.normalBias = 0.08 / 1000;+                shadowGenerator.darkness = -1;+                shadowGenerator.forceBackFacesOnly = true;+                shadowGenerator.frustumEdgeFalloff = 0;++                AdomStatics.shadowGenerator = shadowGenerator;++                // Studio environment map for PBR reflections + skybox.+                // Bundled as base64 so consumers don't need to copy assets.+                // environmentUrl prop lets them override with their own .env file.+                if (environmentUrl) {+                    scene.environmentTexture = CubeTexture.CreateFromPrefilteredData(environmentUrl, scene);+                } else {+                    const raw = atob(STUDIO_ENV_BASE64.split(',')[1]);+                    const bytes = new Uint8Array(raw.length);+                    for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);+                    const blobUrl = URL.createObjectURL(new Blob([bytes], { type: 'application/octet-stream' }));+                    scene.environmentTexture = new CubeTexture(blobUrl, scene, null, false, null, null, null, undefined, true, '.env');+                }++                // createDefaultEnvironment picks up scene.environmentTexture+                // automatically (same code path Hydrogen uses with a URL string).+                // skyboxColor tints the env map to a uniform gray.+                // 3d-viewer-design skill §3b — neutral steel-blue gradient.+                // Top  ≈ #5a6b7e  (0.353, 0.420, 0.494)  ~37% luma+                // Bot  ≈ #2a3340  (0.165, 0.200, 0.251)  ~20% luma+                // Both ends deliberately brighter than near-black so dark IC+                // bodies stay visible in any vertical position.+                const SKILL_BG_TOP = new Color3(0.353, 0.420, 0.494);+                const SKILL_BG_BOTTOM = new Color3(0.165, 0.200, 0.251);+                const SKILL_BG_MID = new Color3(+                    (SKILL_BG_TOP.r + SKILL_BG_BOTTOM.r) * 0.5,+                    (SKILL_BG_TOP.g + SKILL_BG_BOTTOM.g) * 0.5,+                    (SKILL_BG_TOP.b + SKILL_BG_BOTTOM.b) * 0.5,+                );++                const envHelper = scene.createDefaultEnvironment({+                    createGround: false,+                    createSkybox: true,+                    skyboxSize: 5000,+                    skyboxColor: SKILL_BG_MID,+                    enableGroundMirror: false,+                });++                scene.environmentIntensity = 1.0;+                scene.clearColor = new Color4(SKILL_BG_BOTTOM.r, SKILL_BG_BOTTOM.g, SKILL_BG_BOTTOM.b, 1);++                if (envHelper?.skybox) {+                    envHelper.skybox.parent = rootNode;+                }++                if (showViewCube && AdomStatics.camera) {+                    ViewCubeBuilder.createViewCube(scene, AdomStatics.camera, engine, viewCubeCallbacks);+                }++                // 3d-viewer-design §8 axis helpers — non-negotiable.+                // §8a world-origin axes (default ON, toggleable via API)+                // §8c screen-space corner triad (always on)+                if (AdomStatics.camera) {+                    initAxisHelpers(scene, AdomStatics.camera, { worldDefaultOn: true });+                }++                scene.onPointerObservable.add(() => { /* Ensure pointer events */ });++                const canvas = engine.getRenderingCanvas();+                if (canvas) {+                    canvas.addEventListener('contextmenu', (event) => event.preventDefault());+                }++                resolve();+            } catch (error) {+                console.error("SceneBuilder Error during setup:", error);+                reject(error instanceof Error ? error : new Error(String(error)));+            }+        });+    }+}
vendor/adom-step/vendor/adom-3d-viewer-babylon9/src/lib/adom-babylon/3DViewer/ViewCubeBuilder.tsadded+1103
@@ -0,0 +1,1103 @@+import { ArcRotateCamera } from "@babylonjs/core/Cameras/arcRotateCamera";+import { Engine } from "@babylonjs/core/Engines/engine";+import { WebGPUEngine } from "@babylonjs/core/Engines/webgpuEngine";+import { StandardMaterial } from "@babylonjs/core/Materials/standardMaterial";+import { DynamicTexture } from "@babylonjs/core/Materials/Textures/dynamicTexture";+import { Color3, Color4 } from '@babylonjs/core/Maths/math.color';+import { Matrix, Quaternion, Vector3 } from '@babylonjs/core/Maths/math.vector'+import { Viewport } from "@babylonjs/core/Maths/math.viewport";+import { Mesh } from "@babylonjs/core/Meshes/mesh";+import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";+import { Scene } from "@babylonjs/core/scene";+import { Animation, Effect, EngineStore, PickingInfo, Ray, SolidParticle, VertexBuffer } from '@babylonjs/core'+import { SolidParticleSystem } from "@babylonjs/core/Particles/solidParticleSystem";+import { AdomStatics } from "../AdomStatics";+import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight';+import { ShaderMaterial } from '@babylonjs/core/Materials/shaderMaterial'+import { BoundingInfo } from '@babylonjs/core/Culling/boundingInfo'++export interface ViewCubeUICallbacks {+    onTooltip: (text: string | null, x: number, y: number) => void+    onHomeClick: () => void+    onFitClick: () => void+    onViewModeClick: (x: number, y: number) => void+}++export class ViewCubeBuilder {+    private static prevPick: number = -1+    private static prevDarkened: number = -1+    private static particleColorMap: Map<number, Color4> = new Map()++    /**+     * Creates a combined texture atlas for all six faces of the cube+     * @internal+     */+    static _createCombinedTexture(scene: Scene) {+        const size = 512+        const dynamicTexture = new DynamicTexture(+            'CombinedTexture',+            { width: size * 3, height: size * 3 },+            scene,+            true+        )+        const context = dynamicTexture.getContext()+        dynamicTexture.hasAlpha = true++        // Define the face names in the texture atlas layout+        const texts = [+            ['FRONT', 'BACK', 'RIGHT'],+            ['LEFT', 'BOT', 'TOP']+        ]++        // Fill background with white+        context.fillStyle = 'white'+        context.fillRect(0, 0, size * 3, size * 2)++        // Setup text style for face labels+        context.font = '150px Arial'+        context.textAlign = 'center'+        context.textBaseline = 'middle'+        context.fillStyle = 'black'++        // Draw each face text+        for (let row = 0; row < 2; row++) {+            for (let col = 0; col < 3; col++) {+                context.fillText(texts[row][col], (col + 0.5) * size, (row + 0.5) * size)+            }+        }++        // Setup text style for axis labels+        context.font = 'bold 120px Arial'+        context.fillStyle = 'red'+        context.fillText('X', 0.5 * size, (2 + 0.5) * size)++        context.fillStyle = 'green'+        context.fillText('Y', (1 + 0.5) * size, (2 + 0.5) * size)++        context.fillStyle = 'blue'+        context.fillText('Z', (2 + 0.5) * size, (2 + 0.5) * size)++        dynamicTexture.update()+        return dynamicTexture+    }++    /**+     * Normalizes an angle between -PI and PI+     * @internal+     */+    static _normalizeAngle(angle: number): number {+        return angle - Math.PI * 2 * Math.floor((angle + Math.PI) / (2 * Math.PI))+    }++    /**+     * Calculates the shortest angle distance between two angles+     * @internal+     */+    static _shortestAngleDistance(from: number, to: number): number {+        const normalizedFrom = ViewCubeBuilder._normalizeAngle(from)+        const normalizedTo = ViewCubeBuilder._normalizeAngle(to)+        const difference = normalizedTo - normalizedFrom+        if (difference > Math.PI) {+            return difference - Math.PI * 2+        } else if (difference < -Math.PI) {+            return difference + Math.PI * 2+        }+        return difference+    }++    /**+     * Animates camera to a new position+     * @internal+     */+    static _animateCameraToPosition(+        camera: ArcRotateCamera,+        newAlpha: number,+        newBeta: number,+        newRadius: number,+        duration: number+    ) {+        const fps = 60+        const totalFrames = fps * duration+        const targetAlpha =+            camera.alpha + ViewCubeBuilder._shortestAngleDistance(camera.alpha, newAlpha)+        const targetBeta =+            camera.beta + ViewCubeBuilder._shortestAngleDistance(camera.beta, newBeta)++        const alphaAnimation = new Animation(+            'alphaAnimation',+            'alpha',+            fps,+            Animation.ANIMATIONTYPE_FLOAT,+            Animation.ANIMATIONLOOPMODE_CONSTANT+        )++        const alphaKeys = [+            { frame: 0, value: camera.alpha },+            { frame: totalFrames, value: targetAlpha }+        ]+        alphaAnimation.setKeys(alphaKeys)++        const betaAnimation = new Animation(+            'betaAnimation',+            'beta',+            fps,+            Animation.ANIMATIONTYPE_FLOAT,+            Animation.ANIMATIONLOOPMODE_CONSTANT+        )++        const betaKeys = [+            { frame: 0, value: camera.beta },+            { frame: totalFrames, value: targetBeta }+        ]+        betaAnimation.setKeys(betaKeys)++        const radiusAnimation = new Animation(+            'radiusAnimation',+            'radius',+            fps,+            Animation.ANIMATIONTYPE_FLOAT,+            Animation.ANIMATIONLOOPMODE_CONSTANT+        )++        const radiusKeys = [+            { frame: 0, value: camera.radius },+            { frame: totalFrames, value: newRadius }+        ]+        radiusAnimation.setKeys(radiusKeys)++        camera.animations = [alphaAnimation, betaAnimation, radiusAnimation]+        camera.getScene().beginAnimation(camera, 0, totalFrames, false)+    }++    static createViewCube(+        mainScene: Scene,+        mainCamera: ArcRotateCamera,+        engine: WebGPUEngine | Engine,+        uiCallbacks?: ViewCubeUICallbacks+    ) {+        const scene2 = new Scene(engine)+        scene2.autoClear = false+        scene2.useRightHandedSystem = true++        const camera2 = new ArcRotateCamera('camera2', 0, Math.PI / 2, 30, Vector3.Zero(), scene2)+        camera2.upVector = new Vector3(0, 0, 1) // Set Z as up+        camera2.viewport = new Viewport(0.85, 0.85, 0.15, 0.15)++        const light = new HemisphericLight('light', new Vector3(0, 1, 0), scene2)+        light.intensity = 1+        light.groundColor = new Color3(1, 1, 1)+        light.specular = Color3.Black()+++        let homeButtonMesh: Mesh = null!+        let fitButtonMesh: Mesh = null!+        let viewModeButtonMesh: Mesh = null!+        let homeMat: StandardMaterial = null!+        let fitMat: StandardMaterial = null!+        let viewModeMat: StandardMaterial = null!+        let homeTexNormal: DynamicTexture = null!+        let homeTexHover: DynamicTexture = null!+        let fitTexNormal: DynamicTexture = null!+        let fitTexHover: DynamicTexture = null!+        let vmPerspNormal: DynamicTexture = null!+        let vmPerspHover: DynamicTexture = null!+        let vmOrthoNormal: DynamicTexture = null!+        let vmOrthoHover: DynamicTexture = null!+        let currentCameraMode = mainCamera.mode+        let homeHovered = false+        let fitHovered = false+        let viewModeHovered = false+        let buttonsAlpha = 0++        mainScene.afterRender = () => {+            camera2.alpha = mainCamera.alpha+            camera2.beta = mainCamera.beta+            camera2.lowerAlphaLimit = null+            camera2.lowerBetaLimit = null+            camera2.upperBetaLimit = null+            camera2.upperRadiusLimit = null++            // Ensure the ViewCube viewport is at least minVpPx pixels so the+            // cube stays readable at small canvas sizes. The center is kept+            // at (0.925, 0.925) — same as the default — and only shifts when+            // the viewport would exceed the canvas edge.+            const canvasW = engine.getRenderWidth()+            const canvasH = engine.getRenderHeight()+            const minVpPx = 120+            const defaultFrac = 0.15+            const vpNormW = Math.min(Math.max(canvasW * defaultFrac, minVpPx) / canvasW, 0.3)+            const vpNormH = Math.min(Math.max(canvasH * defaultFrac, minVpPx) / canvasH, 0.3)+            const vpX = Math.max(0, Math.min(1 - vpNormW, 0.925 - vpNormW / 2))+            const vpY = Math.max(0, Math.min(1 - vpNormH, 0.925 - vpNormH / 2))+            camera2.viewport = new Viewport(vpX, vpY, vpNormW, vpNormH)++            if (homeButtonMesh) {+                const depth = 9+                const fovHalf = (camera2.fov || 0.8) / 2+                const halfH = depth * Math.tan(fovHalf)+                const vpW = canvasW * camera2.viewport.width+                const vpH = canvasH * camera2.viewport.height+                const aspect = vpW / vpH+                const halfW = halfH * aspect++                // Button layout constants (at default scale, view-space at depth 9)+                const btnDiam = 1.5+                const btnGap = 0.05+                const edgePad = 0.15+                const cubePad = 0.3++                // Everything (cube projection, button size, gaps) scales+                // proportionally with 1/R, keeping the full-screen visual+                // ratio at every viewport size.+                const defaultR = 14+                const cubeProj0 = 2.2 * depth / defaultR+                const totalAtDefault = cubeProj0 + cubePad + btnDiam + edgePad++                let R = defaultR+                if (totalAtDefault > halfW) {+                    // Viewport too narrow — zoom out just enough to fit.+                    // total(R) = (2.2*depth + (cubePad+btnDiam+edgePad)*defaultR) / R+                    const k = 2.2 * depth + (cubePad + btnDiam + edgePad) * defaultR+                    R = Math.min(k / halfW, defaultR * 2.5)+                }+                camera2.radius = R+                const scale = defaultR / R++                const cubeProj = 2.2 * depth / R+                const invViewMat = Matrix.Invert(camera2.getViewMatrix())++                const sz = btnDiam * scale+                const step = (btnDiam + btnGap) * scale+                const cPad = cubePad * scale+                const ePad = edgePad * scale++                // Hard-floor: buttons are ALWAYS to the right of the cube.+                const xMin = cubeProj + cPad + sz / 2+                const xMax = halfW - ePad - sz / 2+                let xCenter: number+                if (xMin <= xMax) {+                    xCenter = (xMin + xMax) / 2+                } else {+                    // Extreme narrow: let buttons clip at viewport edge,+                    // but never move into the cube.+                    xCenter = xMin+                }+                const yTop = halfH - ePad - sz / 2++                // Hover fade: show buttons only when mouse is in the ViewCube viewport+                const vpPixelLeft = camera2.viewport.x * canvasW+                const vpPixelTop = (1 - camera2.viewport.y - camera2.viewport.height) * canvasH+                const mx = mainScene.pointerX+                const my = mainScene.pointerY+                const inViewport = mx >= vpPixelLeft && mx <= vpPixelLeft + vpW &&+                                   my >= vpPixelTop && my <= vpPixelTop + vpH++                const fadeTarget = inViewport ? 1.0 : 0.0+                buttonsAlpha += (fadeTarget - buttonsAlpha) * 0.12+                if (buttonsAlpha < 0.01) buttonsAlpha = 0+                if (buttonsAlpha > 0.99) buttonsAlpha = 1++                // Dismiss tooltip when buttons fade out+                if (!inViewport && uiCallbacks) {+                    uiCallbacks.onTooltip(null, 0, 0)+                }++                const pickable = buttonsAlpha > 0.1+                homeButtonMesh.visibility = buttonsAlpha+                fitButtonMesh.visibility = buttonsAlpha+                viewModeButtonMesh.visibility = buttonsAlpha+                homeButtonMesh.isPickable = pickable+                fitButtonMesh.isPickable = pickable+                viewModeButtonMesh.isPickable = pickable++                homeButtonMesh.scaling.setAll(scale)+                homeButtonMesh.position = Vector3.TransformCoordinates(+                    new Vector3(xCenter, yTop, -depth), invViewMat+                )+                fitButtonMesh.scaling.setAll(scale)+                fitButtonMesh.position = Vector3.TransformCoordinates(+                    new Vector3(xCenter, yTop - step, -depth), invViewMat+                )+                viewModeButtonMesh.scaling.setAll(scale)+                viewModeButtonMesh.position = Vector3.TransformCoordinates(+                    new Vector3(xCenter, yTop - step * 2, -depth), invViewMat+                )+            }++            // Detect external camera mode changes (e.g. API calls)+            if (viewModeMat && mainCamera.mode !== currentCameraMode) {+                currentCameraMode = mainCamera.mode+                const isOrtho = currentCameraMode === 1+                viewModeMat.diffuseTexture = viewModeHovered+                    ? (isOrtho ? vmOrthoHover : vmPerspHover)+                    : (isOrtho ? vmOrthoNormal : vmPerspNormal)+            }++            scene2.render()+        }++        const cr = 2 // Cube radius+        const es = 0.75 // Edge sizing++        const sps = this.generateSPS(scene2, cr, es)++        sps.initParticles = () => {+            this.initParticlePositions(sps, cr, es)+        }++        // Update the label planes to always face the camera+        sps.updateParticle = particle => {+            this.billboardLabelPlanes(particle, camera2)+            return particle+        }++        const viewCubeMesh = sps.buildMesh()++        //TODO - Refactor to add wanted colors to the atlas and then adjust uvs accordingly.+        // Removes the need for the shader material and buffer which will save performance.++        // Shader material is used selectively choose which meshes to apply the texture to.+        const textureAtlas = ViewCubeBuilder._createCombinedTexture(scene2)+        const shaderMaterial = this.createShaderMaterial(scene2)+        shaderMaterial.setTexture('textureSampler', textureAtlas)+        shaderMaterial.alphaMode = Engine.ALPHA_ADD+        viewCubeMesh.material = shaderMaterial++        //Buffer for particle IDs, used to ignore the applied texture+        const NUM_VERTS_PER_PLANE = 4+        const NUM_TEXTURED_PLANES = 9+        const planeVertCount = NUM_VERTS_PER_PLANE * NUM_TEXTURED_PLANES+        const particleIDs = new Float32Array(viewCubeMesh.getTotalVertices())+        for (let i = 0; i < planeVertCount; i++) {+            particleIDs[i] = 2+        }+        const idBuffer = new VertexBuffer(engine, particleIDs, 'particleID', false, false, 1)+        viewCubeMesh.setVerticesBuffer(idBuffer)++        sps.initParticles()+        sps.setParticles()++        // Visibility box is important for picking,+        // Set to same size as cube to ignore axis and the labels planes+        sps.setVisibilityBox(4)+        sps.isVisibilityBoxLocked = true++        // --- Home Button (Fusion 360-style) ---+        homeButtonMesh = MeshBuilder.CreatePlane('homeButton', { size: 1.5 }, scene2)+        homeButtonMesh.billboardMode = Mesh.BILLBOARDMODE_ALL+        homeButtonMesh.isPickable = true+        homeButtonMesh.enablePointerMoveEvents = true++        homeTexNormal = ViewCubeBuilder._makeSvgTexture(scene2, ViewCubeBuilder._homeIconContent(false), false)+        homeTexHover = ViewCubeBuilder._makeSvgTexture(scene2, ViewCubeBuilder._homeIconContent(true), true)++        homeMat = new StandardMaterial('homeButtonMat', scene2)+        homeMat.diffuseTexture = homeTexNormal+        homeMat.emissiveColor = new Color3(1, 1, 1)+        homeMat.disableLighting = true+        homeMat.useAlphaFromDiffuseTexture = true+        homeMat.backFaceCulling = false+        homeButtonMesh.material = homeMat++        // --- Fit / Zoom-to-Fit Button ---+        fitButtonMesh = MeshBuilder.CreatePlane('fitButton', { size: 1.5 }, scene2)+        fitButtonMesh.billboardMode = Mesh.BILLBOARDMODE_ALL+        fitButtonMesh.isPickable = true+        fitButtonMesh.enablePointerMoveEvents = true++        fitTexNormal = ViewCubeBuilder._makeSvgTexture(scene2, ViewCubeBuilder._fitIconContent(false), false)+        fitTexHover = ViewCubeBuilder._makeSvgTexture(scene2, ViewCubeBuilder._fitIconContent(true), true)++        fitMat = new StandardMaterial('fitButtonMat', scene2)+        fitMat.diffuseTexture = fitTexNormal+        fitMat.emissiveColor = new Color3(1, 1, 1)+        fitMat.disableLighting = true+        fitMat.useAlphaFromDiffuseTexture = true+        fitMat.backFaceCulling = false+        fitButtonMesh.material = fitMat++        // --- View Mode Button (Ortho/Perspective toggle) ---+        viewModeButtonMesh = MeshBuilder.CreatePlane('viewModeButton', { size: 1.5 }, scene2)+        viewModeButtonMesh.billboardMode = Mesh.BILLBOARDMODE_ALL+        viewModeButtonMesh.isPickable = true+        viewModeButtonMesh.enablePointerMoveEvents = true++        const isOrthoInit = mainCamera.mode === 1+        vmPerspNormal = ViewCubeBuilder._makeSvgTexture(scene2, ViewCubeBuilder._viewModeIconContent(false, false), false)+        vmPerspHover = ViewCubeBuilder._makeSvgTexture(scene2, ViewCubeBuilder._viewModeIconContent(true, false), true)+        vmOrthoNormal = ViewCubeBuilder._makeSvgTexture(scene2, ViewCubeBuilder._viewModeIconContent(false, true), false)+        vmOrthoHover = ViewCubeBuilder._makeSvgTexture(scene2, ViewCubeBuilder._viewModeIconContent(true, true), true)++        viewModeMat = new StandardMaterial('viewModeButtonMat', scene2)+        viewModeMat.diffuseTexture = isOrthoInit ? vmOrthoNormal : vmPerspNormal+        viewModeMat.emissiveColor = new Color3(1, 1, 1)+        viewModeMat.disableLighting = true+        viewModeMat.useAlphaFromDiffuseTexture = true+        viewModeMat.backFaceCulling = false+        viewModeButtonMesh.material = viewModeMat++        scene2.onPointerDown = function (evt, pickResult) {+            if (pickResult.pickedMesh === homeButtonMesh) {+                uiCallbacks?.onTooltip(null, 0, 0)+                if (uiCallbacks) {+                    uiCallbacks.onHomeClick()+                } else {+                    const x = 1.625, y = -1.625, z = 1.625+                    const adjust = -Math.PI / 2+                    const targetAlpha = Math.atan2(x, y) + adjust+                    const targetBeta = Math.acos(z / Math.sqrt(x * x + y * y + z * z))+                    ViewCubeBuilder._animateCameraToPosition(+                        mainCamera, targetAlpha, targetBeta, mainCamera.radius, 0.5+                    )+                }+                return+            }+            if (pickResult.pickedMesh === fitButtonMesh) {+                uiCallbacks?.onTooltip(null, 0, 0)+                uiCallbacks?.onFitClick()+                return+            }+            if (pickResult.pickedMesh === viewModeButtonMesh) {+                uiCallbacks?.onTooltip(null, 0, 0)+                if (uiCallbacks) {+                    uiCallbacks.onViewModeClick(evt.offsetX, evt.offsetY)+                } else {+                    ;(mainCamera as any).toggleCameraMode()+                    currentCameraMode = mainCamera.mode+                    const isOrtho = currentCameraMode === 1+                    viewModeMat.diffuseTexture = isOrtho ? vmOrthoHover : vmPerspHover+                }+                return+            }+            const id = cubeRayCollision(pickResult)+            if (id) rotateToParticle(id);+        }++        camera2.onViewMatrixChangedObservable.add(() => {+            darkenFromCameraPosition(getClosestParticleId())+            sps.setParticles()+        })++        scene2.onPointerMove = function (evt, pickResult) {+            const pickedMesh = pickResult.pickedMesh+            const isOverHome = pickedMesh === homeButtonMesh+            const isOverFit = pickedMesh === fitButtonMesh+            const isOverViewMode = pickedMesh === viewModeButtonMesh+            const isOverButton = isOverHome || isOverFit || isOverViewMode++            if (isOverHome !== homeHovered) {+                homeHovered = isOverHome+                homeMat.diffuseTexture = homeHovered ? homeTexHover : homeTexNormal+            }+            if (isOverFit !== fitHovered) {+                fitHovered = isOverFit+                fitMat.diffuseTexture = fitHovered ? fitTexHover : fitTexNormal+            }+            if (isOverViewMode !== viewModeHovered) {+                viewModeHovered = isOverViewMode+                const isOrtho = currentCameraMode === 1+                viewModeMat.diffuseTexture = viewModeHovered+                    ? (isOrtho ? vmOrthoHover : vmPerspHover)+                    : (isOrtho ? vmOrthoNormal : vmPerspNormal)+            }+            if (uiCallbacks) {+                if (isOverHome) {+                    uiCallbacks.onTooltip('Home', evt.offsetX, evt.offsetY)+                } else if (isOverFit) {+                    uiCallbacks.onTooltip('Fit All', evt.offsetX, evt.offsetY)+                } else if (isOverViewMode) {+                    uiCallbacks.onTooltip('View Mode', evt.offsetX, evt.offsetY)+                } else {+                    uiCallbacks.onTooltip(null, 0, 0)+                }+            }+            if (isOverButton) {+                if (ViewCubeBuilder.prevPick !== -1) {+                    const clearColor = new Color4(0.8, 0.8, 1, 0.3)+                    const prevDiff = ViewCubeBuilder.particleColorMap+                        .get(ViewCubeBuilder.prevPick)+                        ?.subtract(clearColor)+                    if (prevDiff) sps.particles[ViewCubeBuilder.prevPick].color!.addInPlace(prevDiff)+                    ViewCubeBuilder.prevPick = -1+                    sps.setParticles()+                }+                return+            }+            highlightFromPointer(evt.offsetX, evt.offsetY, pickResult)+        }+        const endMousePos = camera2.viewport.x + camera2.viewport.width+        viewCubeMesh.enablePointerMoveEvents = true++        EngineStore._LastCreatedScene = AdomStatics.scene++        return scene2++        function cubeRayCollision(pickResult: PickingInfo) {+            if (pickResult.faceId === -1) return++            const picked = sps.pickedParticle(pickResult)+            if (!picked) return++            //Recursive picking to only get the box particles+            //We are guaranteed to hit the box because of the visibility box+            //This is necessary to ignore the axis and the labels planes on the edge case+            //where ray will pass through the box, but the other meshes are in the way+            let id = picked.idx+            let result = pickResult+            while (id < 3 || 29 < id) {+                const ray = new Ray(+                    result.pickedPoint!.add(+                        result.ray!.direction.multiplyByFloats(0.001, 0.001, 0.001)+                    ),+                    result.ray!.direction,+                    10+                )+                result = scene2.pickWithRay(ray)!+                id = sps.pickedParticle(result)!.idx+            }++            return id+        }++        function rotateToParticle(id: number) {+            const position = sps.particles[id].position+            const adjust = position.x === 0 && position.y === 0 ? Math.PI/2 : -Math.PI/2+            const targetAlpha = Math.atan2(position.x, position.y) + adjust+            const targetBeta = Math.acos(+                position.z /+                    Math.sqrt(+                        position.x * position.x + position.y * position.y + position.z * position.z+                    )+            )++            if (targetAlpha !== undefined && targetBeta !== undefined) {+                camera2.onViewMatrixChangedObservable.clear()+                const temp_casting = camera2.onViewMatrixChangedObservable.add(() => {+                    const result = scene2.pick(scene2.pointerX, scene2.pointerY)+                    highlightFromPointer(scene2.pointerX, scene2.pointerY, result)+                    sps.setParticles()+                })++                darkenFromCameraPosition(id)+                sps.setParticles()++                sps.particles[id].color!.addInPlace(new Color4(0, 0, 0, 0))++                const animate_dur = 0.5++                ViewCubeBuilder._animateCameraToPosition(+                    mainCamera,+                    targetAlpha,+                    targetBeta,+                    mainCamera.radius,+                    animate_dur+                )++                setTimeout(() => {+                    camera2.onViewMatrixChangedObservable.remove(temp_casting)+                    camera2.onViewMatrixChangedObservable.add(() => {+                        darkenFromCameraPosition(getClosestParticleId())+                        sps.setParticles()+                    })+                    sps.particles[id].color!.subtractInPlace(new Color4(0, 0, 0, 0))+                }, animate_dur * 1000)+            }+        }++        //TODO: Currently using minimum distance for camera -> particle selection, there should be a+        // smarter way by projecting the cube faces onto the sphere and selecting the current region+        function getClosestParticleId() {+            const distances = []+            for (let i = 3; i < 29; i++) {+                distances.push(Vector3.Distance(camera2.position, sps.particles[i].position))+            }+            return distances.indexOf(Math.min(...distances)) + 3+        }++        function darkenFromCameraPosition(id: number) {+            const targetColor = new Color4(0.4, 0.4, 0.4, 1)+            if (id && id !== ViewCubeBuilder.prevDarkened) {+                const prevDiff = ViewCubeBuilder.particleColorMap+                    .get(ViewCubeBuilder.prevDarkened)+                    ?.subtract(targetColor)+                if (prevDiff) sps.particles[ViewCubeBuilder.prevDarkened].color!.addInPlace(prevDiff)+                ViewCubeBuilder.prevDarkened = id+                const diffFromOriginal = ViewCubeBuilder.particleColorMap+                    .get(id)!+                    .subtract(targetColor)+                sps.particles[id].color!.subtractInPlace(diffFromOriginal)+            }+        }++        function highlightFromPointer(xOffset: number, yOffset: number, pickResult: PickingInfo) {+            //Only ray-cast if pointer is in the viewcube viewport.+            //Uncomment other conditions if the viewcube is moved from the top right corner.+            const targetColor = new Color4(0.8, 0.8, 1, 0.3)++            if (pickResult.faceId === -1) {+                const prevDiff = ViewCubeBuilder.particleColorMap+                    .get(ViewCubeBuilder.prevPick)+                    ?.subtract(targetColor)+                if (prevDiff) sps.particles[ViewCubeBuilder.prevPick].color!.addInPlace(prevDiff)+                ViewCubeBuilder.prevPick = -1+            }+            if (+                // camera2.viewport.x < evt.offsetX / engine.getRenderWidth() &&+                // 1 - endMousePosY < evt.offsetY / engine.getRenderHeight() &&+                xOffset / engine.getRenderWidth() < endMousePos &&+                yOffset / engine.getRenderHeight() < 1 - camera2.viewport.y+            ) {+                const id = cubeRayCollision(pickResult)+                if (id && id !== ViewCubeBuilder.prevPick) {+                    const prevDiff = ViewCubeBuilder.particleColorMap+                        .get(ViewCubeBuilder.prevPick)+                        ?.subtract(targetColor)+                    if (prevDiff)+                        sps.particles[ViewCubeBuilder.prevPick].color!.addInPlace(prevDiff)+                    ViewCubeBuilder.prevPick = id+                    const diffFromOriginal = ViewCubeBuilder.particleColorMap+                        .get(id)!+                        .subtract(targetColor)+                    sps.particles[id].color!.subtractInPlace(diffFromOriginal)+                    sps.setParticles()+                }+            }+        }+    }++++    //TODO Fix the math so that the labels don't rotate when rotating overhead the cube+    // alternatively, use a separate sps in billboard mode, but that will use a separate draw call.+    /***+     Manually rotates the labels to face the camera.+     ***/+    private static billboardLabelPlanes(particle: SolidParticle, camera2: ArcRotateCamera) {+        if (particle.idx <= 2) {+            const cameraPosition = camera2.position+            const particlePosition = particle.position++            // Calculate direction vector from particle to camera+            const direction = cameraPosition.subtract(particlePosition).normalize()++            // Use the camera's up vector instead of a fixed (0,1,0)+            const up = camera2.upVector.normalize()+            const right = Vector3.Cross(up, direction).normalize()+            const newUp = Vector3.Cross(direction, right).normalize()++            // Apply rotation to align with camera+            particle.rotationQuaternion = Quaternion.RotationQuaternionFromAxis(+                right,+                newUp,+                direction+            )+        }+    }++    /***+     Initialize particles with proper positions, rotations and UVs+    ***/+    private static initParticlePositions(sps: SolidParticleSystem, cr: number, es: number) {+        // Define UV mappings for the 6 sides of the box in the texture atlas+        // Format: [u1, v1, u2, v2] represents the rectangle in the texture+        const uvMapping: [number, number, number, number][] = [+            [0, 2 / 3, 1 / 3, 1], // FRONT+            [1 / 3, 2 / 3, 2 / 3, 1], // BACK+            [2 / 3, 2 / 3, 1, 1], // RIGHT++            [0, 1 / 3, 1 / 3, 2 / 3], // LEFT+            [1 / 3, 1 / 3, 2 / 3, 2 / 3], // BOTTOM+            [2 / 3, 1 / 3, 1, 2 / 3], // TOP++            [0, 0, 1 / 3.1, 1 / 3.1], // X Label+            [1 / 3.1, 0, 2 / 3.1, 1 / 3.1], // Y Label+            [2 / 3.1, 0, 0.9, 1 / 3.1] // Z Label+        ]++        const labelPlanePositions = [+            [3.5, -2, -2],+            [-2, 3.5, -2],+            [-2, -2, 3.5]+        ]++        const labelPlaneRotations = [+            [0, -Math.PI / 2, Math.PI / 2],+            [Math.PI / 2, 0, 0],+            [0, 0, 0]+        ]++        // Corner positions (relative to center, size = 4)+        const cornerPositions = [+            [-(cr - es / 2), -(cr - es / 2), -(cr - es / 2)],+            [-(cr - es / 2), -(cr - es / 2), cr - es / 2],+            [-(cr - es / 2), cr - es / 2, -(cr - es / 2)],+            [-(cr - es / 2), cr - es / 2, cr - es / 2],+            [cr - es / 2, -(cr - es / 2), -(cr - es / 2)],+            [cr - es / 2, -(cr - es / 2), cr - es / 2],+            [cr - es / 2, cr - es / 2, -(cr - es / 2)],+            [cr - es / 2, cr - es / 2, cr - es / 2]+        ]++        // Edge positions and rotations+        const edgeBoxPositions = [+            // X-aligned edges+            [-(cr - es / 2), -(cr - es / 2), 0],+            [-(cr - es / 2), cr - es / 2, 0],+            [cr - es / 2, -(cr - es / 2), 0],+            [cr - es / 2, cr - es / 2, 0],+            // Y-aligned edges+            [-(cr - es / 2), 0, -(cr - es / 2)],+            [cr - es / 2, 0, -(cr - es / 2)],+            [-(cr - es / 2), 0, cr - es / 2],+            [cr - es / 2, 0, cr - es / 2],+            // Z-aligned edges+            [0, -(cr - es / 2), -(cr - es / 2)],+            [0, cr - es / 2, -(cr - es / 2)],+            [0, -(cr - es / 2), cr - es / 2],+            [0, cr - es / 2, cr - es / 2]+        ]++        const edgeBoxRotations = [+            [0, Math.PI / 2, 0],+            [0, Math.PI / 2, 0],+            [0, Math.PI / 2, 0],+            [0, Math.PI / 2, 0], // Z-aligned+            [0, 0, Math.PI / 2],+            [0, 0, Math.PI / 2],+            [0, 0, Math.PI / 2],+            [0, 0, Math.PI / 2], // Y-aligned+            [0, 0, 0],+            [0, 0, 0],+            [0, 0, 0],+            [0, 0, 0] // X-aligned+        ]++        // Center face positions and rotations (similar to original)+        const facePositions = [+            [0, -cr, 0],+            [0, cr, 0], // Front, Back+            [cr, 0, 0],+            [-cr, 0, 0], // Right, Left+            [0, 0, -cr],+            [0, 0, cr] // Bottom, Top+        ]++        const faceRotations = [+            [Math.PI / 2, 0, 0],+            [-Math.PI / 2, Math.PI, 0],+            [0, Math.PI / 2, Math.PI / 2],+            [0, -Math.PI / 2, -Math.PI / 2],+            [0, Math.PI, Math.PI],+            [0, 0, 0]+        ]++        const edgeCylinderPositions = [+            [-cr, 0, cr],+            [cr, 0, cr],+            [cr, 0, -cr],+            [0, cr, cr],+            [0, -cr, cr],+            [0, cr, -cr],+            [-cr, cr, 0],+            [cr, cr, 0],+            [cr, -cr, 0]+        ]++        const edgeCylinderRotations = [+            [0, 0, 0],+            [0, 0, 0],+            [0, 0, 0],+            [0, 0, Math.PI / 2],+            [0, 0, Math.PI / 2],+            [0, 0, Math.PI / 2],+            [-Math.PI / 2, 0, 0],+            [-Math.PI / 2, 0, 0],+            [-Math.PI / 2, 0, 0]+        ]++        const rgb = [new Color4(1, 0, 0, 1), new Color4(0, 1, 0, 1), new Color4(0, 0, 1, 1)]++        const axisCylinderPositions = [+            [0.5, -2, -2],+            [-2, 0.5, -2],+            [-2, -2, 0.5]+        ]++        const axisCylinderRotations = [+            [0, 0, Math.PI / 2],+            [0, 0, 0],+            [-Math.PI / 2, 0, 0]+        ]++        const axisConePositions = [+            [3, -2, -2],+            [-2, 3, -2],+            [-2, -2, 3]+        ]++        const axisConeRotations = [+            [0, 0, -Math.PI / 2],+            [0, 0, 0],+            [-Math.PI / 2, 0, Math.PI]+        ]++        // Set label plane particles+        for (let i = 0; i < 3; i++) {+            const particle = sps.particles[i]+            particle.scaling.set(2, 2, 2)+            particle.position.set(...(labelPlanePositions[i] as [number, number, number]))+            particle.rotation.set(...(labelPlaneRotations[i] as [number, number, number]))+            particle.uvs.set(...uvMapping[i + 6])+        }++        // Set center face particles+        for (let i = 0; i < 6; i++) {+            const particle = sps.particles[i + 3]+            particle.position.set(...(facePositions[i] as [number, number, number]))+            particle.rotation.set(...(faceRotations[i] as [number, number, number]))+            particle.uvs.set(...uvMapping[i])+            this.particleColorMap.set(i + 3, particle.color!.clone())+        }++        // Set corner particles+        for (let i = 0; i < 8; i++) {+            const particle = sps.particles[i + 9]+            particle.position.set(...(cornerPositions[i] as [number, number, number]))+            const color = new Color4(0.7, 0.7, 0.7, 1)+            particle.color = color+            this.particleColorMap.set(i + 9, color.clone())+        }++        // Set edge box particles+        for (let i = 0; i < 12; i++) {+            const particle = sps.particles[i + 17]+            particle.position.set(...(edgeBoxPositions[i] as [number, number, number]))+            particle.rotation.set(...(edgeBoxRotations[i] as [number, number, number]))+            const color = new Color4(0.8, 0.8, 0.8, 1)+            particle.color = color+            this.particleColorMap.set(i + 17, color.clone())+        }++        // Set edge cylinder particles+        for (let i = 0; i < 9; i++) {+            const particle = sps.particles[i + 29]+            particle.position.set(...(edgeCylinderPositions[i] as [number, number, number]))+            particle.rotation.set(...(edgeCylinderRotations[i] as [number, number, number]))+            const color = new Color4(0, 0, 0, 1)+            particle.color = color+            this.particleColorMap.set(i + 29, color.clone())+        }++        // Set axis cylinder particles+        for (let i = 0; i < 3; i++) {+            const particle = sps.particles[i + 38]+            particle.position.set(...(axisCylinderPositions[i] as [number, number, number]))+            particle.rotation.set(...(axisCylinderRotations[i] as [number, number, number]))+            particle.color = rgb[i]+        }++        // Set axis cone particles+        for (let i = 0; i < 3; i++) {+            const particle = sps.particles[i + 41]+            particle.position.set(...(axisConePositions[i] as [number, number, number]))+            particle.rotation.set(...(axisConeRotations[i] as [number, number, number]))+            particle.color = rgb[i]+        }+    }++    /*** Adds all the required meshes to the SPS ***/+    private static generateSPS(scene2: Scene, cr: number, es: number) {+        const cornerCubeMesh = MeshBuilder.CreateBox('cornerBase', { size: es }, scene2)+        const edgeBoxMesh = MeshBuilder.CreateBox(+            'edgeBase',+            {+                width: 2 * cr - 2 * es,+                height: es,+                depth: es+            },+            scene2+        )+        const centerPlaneMesh = MeshBuilder.CreatePlane('plane', { size: 2 * cr - 2 * es }, scene2)+        centerPlaneMesh.flipFaces()++        const cylinderMesh = MeshBuilder.CreateCylinder(+            'cylinderBase',+            { height: 4, diameterTop: 0.015, diameterBottom: 0.015 },+            scene2+        )+        const coneMesh = MeshBuilder.CreateCylinder(+            'coneBase',+            { height: 1, diameterTop: 0, diameterBottom: 0.6 },+            scene2+        )+        const axisCylinderMesh = MeshBuilder.CreateCylinder(+            'axisCylinderBase',+            { height: 5, diameterTop: 0.3, diameterBottom: 0.3 },+            scene2+        )++        // Create SPS for all the elements+        const sps = new SolidParticleSystem('viewCubeSPS', scene2, { isPickable: true })++        // Add shapes to SPS+        const _labelPlanesIdx = sps.addShape(centerPlaneMesh, 3)+        const _centerPlanesIdx = sps.addShape(centerPlaneMesh, 6) // 6 center faces+        const _cornerCubesIdx = sps.addShape(cornerCubeMesh, 8) // 8 corners+        const _edgeBoxesIdx = sps.addShape(edgeBoxMesh, 12) // 12 edges+        const _edgeCylindersIdx = sps.addShape(cylinderMesh, 9)+        const _axisCylindersIdx = sps.addShape(axisCylinderMesh, 3)+        const _axisConesIdx = sps.addShape(coneMesh, 3)++        // Dispose of base meshes as they're no longer needed+        cornerCubeMesh.dispose()+        edgeBoxMesh.dispose()+        centerPlaneMesh.dispose()+        cylinderMesh.dispose()+        coneMesh.dispose()+        axisCylinderMesh.dispose()+        return sps+    }++    static createShaderMaterial(scene: Scene) {+        // Load shaders into Babylon.js+        Effect.ShadersStore['customVertexShader'] = `+        precision highp float;+        attribute vec3 position;+        attribute vec3 normal;+        attribute vec2 uv;+        attribute vec4 color;+        attribute float particleID;+        +        uniform mat4 world;+        uniform mat4 worldView;+        uniform mat4 worldViewProjection;+        +        varying vec2 vUV;+        varying vec4 vColor;+        varying float vParticleID;+        +        void main() {+            vUV = uv;+            vColor = color;+            vParticleID = particleID; // Pass the ID to fragment shader+            gl_Position = worldViewProjection * vec4(position, 1.0);+        }`++        Effect.ShadersStore['customFragmentShader'] = `+        precision highp float;+        uniform sampler2D textureSampler;+        varying vec2 vUV;+        varying vec4 vColor;+        varying float vParticleID;+        +        void main() {+            if (vParticleID < 1.0) {  +                gl_FragColor = vColor;+            } else {+                vec4 texColor = texture2D(textureSampler, vUV);+                if (texColor.a < 0.1) discard;+                gl_FragColor = texColor * vColor;+            }+        }`++        // Create Shader Material+        const shaderMaterial = new ShaderMaterial(+            'shaderMat',+            scene,+            {+                vertex: 'custom',+                fragment: 'custom'+            },+            {+                attributes: ['position', 'normal', 'uv', 'color', 'particleID'],+                uniforms: ['world', 'worldView', 'worldViewProjection'],+                samplers: ['textureSampler']+            }+        )++        shaderMaterial.setColor3('emissiveColor', new Color3(1, 1, 1))+        return shaderMaterial+    }++    /**+     * Creates a DynamicTexture by rasterising an SVG via an Image element.+     * 512x512 source with mipmaps for crisp quality at any display size.+     * @internal+     */+    private static _svgTexId = 0+    private static _makeSvgTexture(scene: Scene, iconContent: string, hovered: boolean): DynamicTexture {+        const bg = hovered ? '#505860' : '#373d43'+        const bgOp = hovered ? '0.95' : '0.85'+        const border = hovered ? '#00b8b0' : '#ffffff'+        const borderOp = hovered ? '0.6' : '0.25'+        const svg =+            `<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 128 128">` ++            `<circle cx="64" cy="64" r="54" fill="${bg}" fill-opacity="${bgOp}" stroke="${border}" stroke-opacity="${borderOp}" stroke-width="2.5"/>` ++            iconContent ++            `</svg>`++        const size = 1024+        const tex = new DynamicTexture(`svgIcon_${ViewCubeBuilder._svgTexId++}`, size, scene, true)+        tex.hasAlpha = true+        tex.anisotropicFilteringLevel = 16++        const url = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg)+        const img = new Image(size, size)+        img.onload = () => {+            const ctx = tex.getContext() as unknown as CanvasRenderingContext2D+            ctx.clearRect(0, 0, size, size)+            ctx.drawImage(img, 0, 0, size, size)+            tex.update()+        }+        img.src = url+        return tex+    }++    /** @internal */+    private static _homeIconContent(hovered: boolean): string {+        const c = hovered ? '#00b8b0' : '#ffffff'+        const op = hovered ? '1' : '0.85'+        const bg = hovered ? '#505860' : '#373d43'+        // House: triangle roof + rectangular body + door cutout+        return (+            `<path d="M64 30 L34 54 L94 54 Z" fill="${c}" fill-opacity="${op}"/>` ++            `<rect x="40" y="54" width="48" height="34" fill="${c}" fill-opacity="${op}"/>` ++            `<rect x="55" y="66" width="18" height="22" rx="1" fill="${bg}"/>`+        )+    }++    /** @internal */+    private static _fitIconContent(hovered: boolean): string {+        const c = hovered ? '#00b8b0' : '#ffffff'+        const op = hovered ? '1' : '0.85'+        // Four corner brackets + center square+        return (+            `<path d="M38 50 L38 38 L50 38" fill="none" stroke="${c}" stroke-opacity="${op}" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>` ++            `<path d="M78 38 L90 38 L90 50" fill="none" stroke="${c}" stroke-opacity="${op}" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>` ++            `<path d="M38 78 L38 90 L50 90" fill="none" stroke="${c}" stroke-opacity="${op}" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>` ++            `<path d="M78 90 L90 90 L90 78" fill="none" stroke="${c}" stroke-opacity="${op}" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>` ++            `<rect x="55" y="55" width="18" height="18" rx="2" fill="${c}" fill-opacity="${op}"/>`+        )+    }++    /** @internal */+    private static _viewModeIconContent(hovered: boolean, isOrtho: boolean): string {+        const c = hovered ? '#00b8b0' : '#ffffff'+        const op = hovered ? '1' : '0.85'+        if (isOrtho) {+            // Isometric cube — all depth lines parallel (orthographic)+            return (+                `<path d="M36 86 L74 86 L74 52 L36 52 Z" fill="${c}" fill-opacity="0.12" stroke="${c}" stroke-opacity="${op}" stroke-width="3" stroke-linejoin="round"/>` ++                `<path d="M74 52 L88 38 L88 72 L74 86" fill="${c}" fill-opacity="0.08" stroke="${c}" stroke-opacity="${op}" stroke-width="3" stroke-linejoin="round"/>` ++                `<path d="M36 52 L50 38 L88 38 L74 52" fill="${c}" fill-opacity="0.04" stroke="${c}" stroke-opacity="${op}" stroke-width="3" stroke-linejoin="round"/>`+            )+        } else {+            // Perspective cube — depth lines converge (back face smaller)+            return (+                `<path d="M34 88 L76 88 L76 52 L34 52 Z" fill="${c}" fill-opacity="0.12" stroke="${c}" stroke-opacity="${op}" stroke-width="3" stroke-linejoin="round"/>` ++                `<path d="M76 52 L82 40 L82 68 L76 88" fill="${c}" fill-opacity="0.08" stroke="${c}" stroke-opacity="${op}" stroke-width="3" stroke-linejoin="round"/>` ++                `<path d="M34 52 L46 40 L82 40 L76 52" fill="${c}" fill-opacity="0.04" stroke="${c}" stroke-opacity="${op}" stroke-width="3" stroke-linejoin="round"/>`+            )+        }+    }+}\ No newline at end of file
vendor/adom-step/vendor/adom-3d-viewer-babylon9/src/lib/adom-babylon/AdomStatics.tsadded+90
@@ -0,0 +1,90 @@+import { Engine } from '@babylonjs/core/Engines/engine';+import { Scene } from '@babylonjs/core/scene';+import { AdomCamera } from './Camera/AdomCamera';+import type { Nullable } from '@babylonjs/core/types';+import type { ShadowGenerator, ISceneLoaderAsyncResult } from '@babylonjs/core';+import type { SpotLight } from '@babylonjs/core/Lights/spotLight';+import type { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight';+import { RenderTargetTexture } from '@babylonjs/core';++export class AdomStatics {+    public static engine: Nullable<Engine> = null;+    public static scene: Nullable<Scene> = null;+    public static camera: Nullable<AdomCamera> = null;+    public static shadowGenerator: Nullable<ShadowGenerator> = null;+    public static spotLight: Nullable<SpotLight> = null;+    /** Bottom-fill hemispheric light — illuminates the underside of parts.+     *  Default OFF (intensity 0). Toggleable via ThreeDViewer.setBottomLight(). */+    public static bottomLight: Nullable<HemisphericLight> = null;++    static modelLoadingPromises = new Map<string, Promise<ISceneLoaderAsyncResult>>();++    private static _windowResizeHandler: Nullable<EventListener> = null;++    /**+     * Initialize the engine and scene.+     * @param canvas - The canvas element to render to+     * @param zUp - If true, use Z-up coordinate system (default). If false, use Y-up (Babylon default).+     */+    public static async init(canvas: HTMLCanvasElement, zUp: boolean = true): Promise<void> {+        if (this.engine && this.scene && this.camera) {+            return;+        }++        this.engine = new Engine(canvas, true, { useHighPrecisionFloats: true });+        this.scene = new Scene(this.engine);+        this.scene.useRightHandedSystem = zUp;++        this.camera = new AdomCamera(this.scene, canvas, zUp);+        this.camera.disableBounds();+        this.camera.setClickToSetRotationPoint(false);+        this.scene.activeCamera = this.camera;+    }++    public static dispose(): void {+        if (this.scene) {+            this.scene.dispose();+            this.scene = null;+        }+        if (this.engine) {+            this.engine.stopRenderLoop();+            this.engine.dispose();+            this.engine = null;+        }+        if (this.camera) {+            this.camera.dispose();+            this.camera = null;+        }+        if (this.shadowGenerator) {+            this.shadowGenerator.dispose();+            this.shadowGenerator = null;+        }+        if (this.spotLight) {+            this.spotLight.dispose();+            this.spotLight = null;+        }+        if (this.bottomLight) {+            this.bottomLight.dispose();+            this.bottomLight = null;+        }+        if (this.modelLoadingPromises) {+            this.modelLoadingPromises.clear();+        }+    }++    /**+     * Re-renders the shadow map once. Defers by one frame so that any light+     * frustum or caster-list changes from the current frame are applied first.+     */+    public static refreshShadows(): void {+        if (!this.shadowGenerator || !this.scene) return;+        const sg = this.shadowGenerator;+        const shadowMap = sg.getShadowMap();+        if (!shadowMap) return;++        this.scene.onAfterRenderObservable.addOnce(() => {+            shadowMap.refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;+            shadowMap.resetRefreshCounter();+        });+    }+}
vendor/adom-step/vendor/adom-3d-viewer-babylon9/src/lib/adom-babylon/Camera/AdomCamera.tsadded+1458
@@ -0,0 +1,1458 @@+import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera';+import { Vector3 } from '@babylonjs/core/Maths/math.vector';+import { Scene } from '@babylonjs/core/scene';+import { KeyboardEventTypes, PointerEventTypes, type IKeyboardEvent } from '@babylonjs/core/Events';+import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';+import { HighlightLayer } from '@babylonjs/core/Layers/highlightLayer';+import { Color3 } from '@babylonjs/core/Maths/math.color'+import { Animation } from '@babylonjs/core/Animations/animation';+import { QuadraticEase, EasingFunction } from '@babylonjs/core/Animations/easing';+import { AdomStatics } from '../AdomStatics';+import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';+import { writable, get } from 'svelte/store';++/** When true, disables camera keyboard input (WASD). Use when another control has focus (e.g. input fields). */+export const disableCameraKeyboardStore = writable<boolean>(false);+++export class AdomCamera extends ArcRotateCamera{++    // State for keyboard and mouse handling+    private isPointerDownWithShiftAlt: boolean = false;+    private lastCamTarget: Vector3 | null = null;+    private lastCamTargetMove: any;+    private sphere: any;+    private sphereHighlightLayer: HighlightLayer | undefined;+    private isRotating: boolean = false;+    private sphereVisibilityThreshold: number = 500; // ms to keep sphere visible after rotation stops+    // Controls whether normal left-clicks behave like shift+alt+click+    private useClickToSetRotationPoint: boolean = false;+    private clickStartTime: number = 0; // To track when the click started+    private clickDelayTimeout: NodeJS.Timeout | null = null; // For delaying click-to-set behavior+    private clickDelayDuration: number = 500; // 0.5 second delay for click-to-set (reduced from 1 second)+    // Orbiting with spacebar properties+    private isSpacebarPressed: boolean = false;+    private orbitAnimationFrameId: number | null = null;+    private orbitSpeed: number = 0.005; // radians per frame+    // First-person movement properties+    private moveSpeed: number = 2;+    private keysPressed: { [key: string]: boolean } = {+        'w': false,+        's': false,+        'a': false,+        'd': false,+        'q': false,+        'e': false+    };+    // Precision mode (toggle with Caps Lock)+    private precisionMode: boolean = false;+    // Base speeds (normal mode)+    private baseOrbitSpeed: number = 0.005;+    private baseMoveSpeed: number = 2;+    // Camera bounds properties+    private targetBounds: {+        minX: number | null;+        maxX: number | null;+        minY: number | null;+        maxY: number | null;+        minZ: number | null;+        maxZ: number | null;+    } = {+        minX: null,+        maxX: null,+        minY: null,+        maxY: null,+        minZ: null,+        maxZ: null+    };+    private enforceBounds: boolean = false;++    // Camera rotation detection for middle mouse+    private lastAlpha: number = 0;+    private lastBeta: number = 0;+    private lastRadius: number = 0;+    private isMiddleMouseRotating: boolean = false;+    private rotationDetectionTimeout: NodeJS.Timeout | null = null;+    private isMiddleMouseDown: boolean = false;+    +    // Pinch zoom sensitivity (multiplier for trackpad pinch gestures)+    private pinchZoomMultiplier: number = 1.0;+    private canvas: HTMLCanvasElement | null = null;+    private wheelHandler: ((e: WheelEvent) => void) | null = null;+    +    // Right-click drag panning+    private isRightMouseDown: boolean = false;+    private isCtrlLeftPanning: boolean = false;+    private lastPointerX: number = 0;+    private lastPointerY: number = 0;+    private panSpeed: number = 1.0;++    // Event listeners for external control (e.g. automated tests or external UI)+    private externalEventListeners: Map<string, EventListener> = new Map();++    /***+     * Create a new AdomCamera+     * @param scene: The scene that the camera is in+     * @param canvas: The canvas that the camera is in+     * @param zUp: If true (default), Z-axis is up. If false, Y-axis is up (Babylon default).+    */+    constructor(scene: Scene, canvas: HTMLCanvasElement, zUp: boolean = true) {+        // console.log("adom camera start");+        // Create the camera+        super("adomCamera", 0, 0, 2500, new Vector3(0, 0, 140), scene);++        this.upVector = zUp ? new Vector3(0, 0, 1) : new Vector3(0, 1, 0);+        this.canvas = canvas;+        this.attachControl(canvas, true);+        this.parent = null;++        // Additional AdomCamera initialization+        this.setupCameraDefaults();+        this.setupKeyboardEvents();+        this.setupSphereAndHighlight();+        this.setupPointerEvents();+        this.setupPinchZoom();+        +        // Initialize rotation detection values AFTER camera setup+        this.initializeRotationDetection();+        +        this.setupExternalEventListeners();+        // console.log("adom camera end");++    }++    /***+    * Getter to expose the ArcRotateCamera+    */+    public getCamera(): ArcRotateCamera {+        return this;+    }++    /***+    * Get the current coordinates of the camera+    */+    public getCameraCoords() {+        // console.log("Camera position:", this.position);+        // console.log("Camera target:", this.target);+        // console.log("Camera radius:", this.radius);+        // console.log("Camera alpha:", this.alpha);+        // console.log("Camera beta:", this.beta);++        let str = +        `Camera.moveCamTo(+        0, 0, 0, // Position values are ignored - will be calculated from other parameters+        ${this.target.x}, ${this.target.y}, ${this.target.z}, // target - what the camera looks at+        ${this.radius}, ${this.alpha}, ${this.beta}, // radius, alpha, beta - determine camera position+        60, // Optional: fps (defaults to 60)+        60); // Optional: totalframes (defaults to 60)`;++        navigator.clipboard?.writeText(str);+        +        // console.log("Camera coordinates copied to clipboard");+        return str;+    }++    /***+    * Set up the camera to work in our environment+    */+    private setupCameraDefaults() {+        this.allowUpsideDown = true;+        this.panningSensibility = 0; // Disable built-in panning (0 = disabled in Babylon)+        this.panningInertia = 0;+        this.invertRotation = false;+        this.panningAxis = new Vector3(1, 1, 0);+        this.inertia = 0.1;+        this.angularSensibilityX = 500;+        this.angularSensibilityY = 500;+        this.lowerAlphaLimit = -100;+        this.lowerBetaLimit = -100;+        this.lowerRadiusLimit = -100;+        this.upperRadiusLimit = 2700;+        this.wheelDeltaPercentage = 0.1;+        // minZ is set here, maxZ is left at Babylon default for perspective mode+        // Orthographic mode dynamically adjusts both in updateClippingPlanes()+        this.minZ = this.radius / 1000;+        // Note: NOT setting maxZ here - Babylon's default works better for perspective+        // upVector already set in constructor from zUp option+        this.alpha = Math.PI / 2;+        this.beta = Math.PI / 4;+        this.zoomToMouseLocation = true;+        +        // Load camera mode from localStorage, default to perspective+        const savedMode = typeof window !== 'undefined' ? localStorage.getItem('adom_camera_mode') : null;+        if (savedMode === 'orthographic') {+            this.mode = ArcRotateCamera.ORTHOGRAPHIC_CAMERA;+            this.wasOrthoMode = true; // Initialize tracking flag+            // console.log('Camera initialized in Orthographic mode (from localStorage)');+        } else {+            this.mode = ArcRotateCamera.PERSPECTIVE_CAMERA;+            // console.log('Camera initialized in Perspective mode');+        }+        // Initial orthographic bounds - these will be updated based on radius+        this.updateOrthographicBounds();+        +        // Register an observer to check bounds on every frame+        this._scene.onBeforeRenderObservable.add(() => {+            if (this.enforceBounds) {+                this.checkAndEnforceBounds();+            }+            +            // Handle keyboard movement+            this.processKeyboardInput();+            +            // Detect middle mouse rotation and show orbit sphere+            this.detectAndShowRotationSphere();+            +            // Update orthographic bounds and clipping planes based on radius for proper zooming+            this.updateClippingPlanes();+        });+    }++    /**+     * Update orthographic bounds based on camera radius for proper zooming+     */+    public updateOrthographicBounds(): void {+        // Scale orthographic bounds based on radius+        // Smaller radius = more zoomed in (smaller bounds)+        const scale = this.radius / 2;+        const aspect = this._scene.getEngine().getRenderWidth() / this._scene.getEngine().getRenderHeight();+        +        this.orthoLeft = -scale * aspect;+        this.orthoRight = scale * aspect;+        this.orthoTop = scale;+        this.orthoBottom = -scale;+    }+    +    // Track whether we've set ortho clipping to know when to restore perspective values+    private wasOrthoMode: boolean = false;+    +    /**+     * Update clipping planes based on camera mode+     * Orthographic mode requires different clipping plane calculations to prevent+     * excessive near-plane clipping while maintaining good z-buffer precision+     */+    private updateClippingPlanes(): void {+        if (this.mode === ArcRotateCamera.ORTHOGRAPHIC_CAMERA) {+            this.updateOrthographicBounds();+            // In orthographic mode, use negative minZ to prevent near-plane clipping+            // when viewing at angles. Objects "in front" of the camera plane need to be visible.+            this.minZ = -this.radius * 20;+            this.maxZ = this.radius * 20;+            this.wasOrthoMode = true;+        } else {+            if (this.wasOrthoMode) {+                this.wasOrthoMode = false;+            }+            // Scale minZ with radius to maintain good depth buffer precision at all zoom levels+            this.minZ = this.radius / 1000;+        }+    }++    /***+    * Set up key inputs for our environment+    */+    private setupKeyboardEvents() {+        this._scene.onKeyboardObservable.add((kbInfo) => {+            switch (kbInfo.type) {+                case KeyboardEventTypes.KEYDOWN:+                    this.handleKeyDown(kbInfo.event);+                    break;+                case KeyboardEventTypes.KEYUP:+                    this.handleKeyUp(kbInfo.event);+                    break;+            }+        });+    }++    /***+    * Helper for keyboard events+    */+    private handleKeyDown(event: IKeyboardEvent) {+        const key = event.key.toLowerCase();+        const code = event.code;+        +        // Toggle precision mode on Caps Lock press+        if (code === 'CapsLock') {+            this.precisionMode = !this.precisionMode;+            this.updateSpeeds();+            return;+        }+        +        if (get(disableCameraKeyboardStore)) {+            return;+        }+        +        // Handle spacebar for orbit+        if (key === ' ' || code === 'Space') {+            if (!this.isSpacebarPressed) {+                this.isSpacebarPressed = true;+                this.startOrbiting();+            }+            return;+        }+        +        // Track WASD key presses+        if (key in this.keysPressed) {+            this.keysPressed[key] = true;+        }+    }++    private handleKeyUp(event: IKeyboardEvent) {+        const key = event.key.toLowerCase();+        const code = event.code;+        +        if (key === ' ' || code === 'Space') {+            this.isSpacebarPressed = false;+            this.stopOrbiting();+            return;+        }+        +        // Track WASD key releases+        if (key in this.keysPressed) {+            this.keysPressed[key] = false;+        }+    }+    +    /**+     * Updates movement and orbit speeds based on precision mode+     */+    private updateSpeeds(): void {+        if (this.precisionMode) {+            // Precision mode (1/4 speed)+            this.orbitSpeed = this.baseOrbitSpeed / 4;+            this.moveSpeed = this.baseMoveSpeed / 4;+            // console.log('Precision mode activated: speed reduced to 25%');+        } else {+            // Normal speed+            this.orbitSpeed = this.baseOrbitSpeed;+            this.moveSpeed = this.baseMoveSpeed;+            // console.log('Normal speed restored');+        }+    }++    /**+     * Start orbiting around the target point+     */+    private startOrbiting() {+        // Show the rotation sphere at the target position+        this.showRotationSphere();++        // Start the animation if it's not already running+        if (this.orbitAnimationFrameId === null) {+            // Use requestAnimationFrame for smooth animation+            const animate = () => {+                if (this.isSpacebarPressed) {+                    // Increment alpha to orbit around the target+                    this.alpha += this.orbitSpeed;+                    +                    // Keep sphere visible and properly sized+                    this.updateRotationSphere();+                    +                    // Continue animation+                    this.orbitAnimationFrameId = requestAnimationFrame(animate);+                }+            };+            +            this.orbitAnimationFrameId = requestAnimationFrame(animate);+        }+    }++    /**+     * Stop orbiting animation+     */+    private stopOrbiting() {+        // Cancel the animation frame+        if (this.orbitAnimationFrameId !== null) {+            cancelAnimationFrame(this.orbitAnimationFrameId);+            this.orbitAnimationFrameId = null;+        }+        +        // Hide the rotation sphere+        if (this.sphere) {+            this.sphere.isVisible = false;+        }+    }++    /**+     * Set the orbit speed +     * @param speed The speed in radians per frame+     */+    public setOrbitSpeed(speed: number): void {+        this.baseOrbitSpeed = speed;+        // Apply precision mode slowdown if active+        this.orbitSpeed = this.precisionMode ? speed / 4 : speed;+    }++    /**+     * Get the current orbit speed+     */+    public getOrbitSpeed(): number {+        return this.orbitSpeed;+    }++    /**+     * Initialize rotation detection values to current camera state+     */+    private initializeRotationDetection(): void {+        this.lastAlpha = this.alpha;+        this.lastBeta = this.beta;+        this.lastRadius = this.radius;+        this.isMiddleMouseRotating = false;+        this.isMiddleMouseDown = false;+        +        // Make sure sphere is hidden initially+        if (this.sphere) {+            this.sphere.isVisible = false;+        }+        // console.log("Rotation detection initialized");+    }+    +    /**+     * Set up event listeners for external control (e.g. automated tests or external UI).+     * Trigger via window.dispatchEvent(new CustomEvent('camera:change_view', { detail: { orientation: 'top' } })).+     */+    private setupExternalEventListeners(): void {+        // Create a single event listener that handles all orientations+        const changeViewListener = (event: Event) => {+            const customEvent = event as CustomEvent;+            const orientation = customEvent.detail?.orientation;+            +            if (!orientation) {+                // console.error('camera:change_view event requires orientation in detail');+                return;+            }+            +            switch (orientation) {+                case 'top':+                    this.rotateToCubeTop();+                    break;+                case 'top_front_right':+                    this.rotateToCubeTopFrontRight();+                    break;+                case 'top_front_left':+                    this.rotateToCubeTopFrontLeft();+                    break;+                case 'top_back_right':+                    this.rotateToCubeTopBackRight();+                    break;+                case 'top_back_left':+                    this.rotateToCubeTopBackLeft();+                    break;+                default:+                    // console.error(`Unknown camera orientation: ${orientation}`);+            }+        };+        +        // Store listener for cleanup+        this.externalEventListeners.set('camera:change_view', changeViewListener as EventListener);+        +        // Add event listener to window+        if (typeof window !== 'undefined') {+            window.addEventListener('camera:change_view', changeViewListener as EventListener);+            // console.log('Camera external event listener registered for camera:change_view');+        }+    }++    /**+     * Reset middle mouse detection state to ensure clean detection+     */+    private resetMiddleMouseState(): void {+        // console.log("Resetting middle mouse state for clean detection");+        this.isMiddleMouseRotating = false;+        // Don't reset this.isMiddleMouseDown as it tracks actual button state+        +        // Clear finished animations using robust checking+        if (this.lastCamTargetMove) {+            const animatable = this.lastCamTargetMove;+            const shouldClear = animatable.animationEnded || +                              !animatable.animationStarted ||+                              !animatable.runtimeAnimations ||+                              animatable.runtimeAnimations.length === 0 ||+                              animatable.runtimeAnimations.every((anim: any) => !anim.animation || anim.animation._stopped);+            +            if (shouldClear) {+                // console.log("Clearing finished animation during reset");+                this.lastCamTargetMove = null;+            }+        }+        +        // Update last known camera values to current state+        this.lastAlpha = this.alpha;+        this.lastBeta = this.beta;+        this.lastRadius = this.radius;+    }++    /**+     * Detect camera rotation (middle mouse) and show/hide rotation sphere accordingly+     */+    private detectAndShowRotationSphere(): void {+        const currentAlpha = this.alpha;+        const currentBeta = this.beta;+        const currentRadius = this.radius;+        +        // Check if camera rotation values have changed significantly+        const alphaChanged = Math.abs(currentAlpha - this.lastAlpha) > 0.001;+        const betaChanged = Math.abs(currentBeta - this.lastBeta) > 0.001;+        const radiusChanged = Math.abs(currentRadius - this.lastRadius) > 0.1;+        +        const isRotatingNow = alphaChanged || betaChanged || radiusChanged;+        +        // Check if animation is actually still running+        let isAnimationRunning = false;+        if (this.lastCamTargetMove) {+            // Check multiple properties to determine if animation is truly finished+            const animatable = this.lastCamTargetMove;+            isAnimationRunning = animatable.animationStarted && +                               !animatable.animationEnded && +                               animatable.runtimeAnimations && +                               animatable.runtimeAnimations.length > 0 &&+                               animatable.runtimeAnimations.some((anim: any) => anim.animation && !anim.animation._stopped);+        }+        +        const canDetectMiddleMouse = !this.isSpacebarPressed && +                                   !this.isPointerDownWithShiftAlt && +                                   !this.isRotating && +                                   !isAnimationRunning;+        +        if (this.isMiddleMouseDown && !this.isMiddleMouseRotating && canDetectMiddleMouse) {+            // Check if camera is rotating significantly+            if (isRotatingNow) {+                this.isMiddleMouseRotating = true;+                this.showRotationSphere();+                // console.log("Middle mouse rotation detected - showing orbit sphere");+            }+        } else if (this.isMiddleMouseDown && !this.isMiddleMouseRotating && !canDetectMiddleMouse) {+            // Debug: log what's blocking detection+            // console.log("Middle mouse blocked - spacebar:", this.isSpacebarPressed, +            //            "shiftAlt:", this.isPointerDownWithShiftAlt,+            //            "leftRotating:", this.isRotating,+            //            "animation:", !!this.lastCamTargetMove,+            //            "animRunning:", isAnimationRunning);+        }+        +        // Clean up finished animations - use more robust checking+        if (this.lastCamTargetMove) {+            const animatable = this.lastCamTargetMove;+            const shouldClear = animatable.animationEnded || +                              !animatable.animationStarted ||+                              !animatable.runtimeAnimations ||+                              animatable.runtimeAnimations.length === 0 ||+                              animatable.runtimeAnimations.every((anim: any) => !anim.animation || anim.animation._stopped);+            +            if (shouldClear) {+                // console.log("Clearing finished camera animation");+                this.lastCamTargetMove = null;+            }+        }+        +        // Handle middle mouse rotation sphere display+        if (this.isMiddleMouseRotating) {+            if (this.isMiddleMouseDown) {+                // Keep updating sphere position during rotation+                this.updateRotationSphere();+                +                // Clear any pending timeout since we're still rotating+                if (this.rotationDetectionTimeout) {+                    clearTimeout(this.rotationDetectionTimeout);+                    this.rotationDetectionTimeout = null;+                }+            } else {+                // Middle mouse was released - hide sphere immediately+                this.isMiddleMouseRotating = false;+                if (this.sphere) {+                    this.sphere.isVisible = false;+                }+                // console.log("Middle mouse released - hiding orbit sphere");+            }+        }+        +        // Update last known values+        this.lastAlpha = currentAlpha;+        this.lastBeta = currentBeta;+        this.lastRadius = currentRadius;+    }++    /***+    * Glowing sphere for showing where the rotation axis is+    */+    private setupSphereAndHighlight() {+        // Create a sphere to show the point where the camera is rotating around+        this.sphere = MeshBuilder.CreateSphere('arcRotateIndicatorSphere', { segments: 10, diameter: 1 }, this._scene);+        this.sphere.isVisible = false;++        const material = new StandardMaterial("rotationSphereMaterial", this._scene);+        material.emissiveColor = new Color3(0, 0.38, 0.94);+        this.sphere.material = material;+    }++    /***+    * Set up pointer event inputs for our environment+    */+    private setupPointerEvents() {+        let rotationTimeout: NodeJS.Timeout | null = null;++        this._scene.onPointerObservable.add((pointerInfo) => {+            switch (pointerInfo.type) {+                case PointerEventTypes.POINTERDOWN:+                    // Track right mouse button for custom panning+                    if (pointerInfo.event.button === 2) {+                        this.isRightMouseDown = true;+                        this.lastPointerX = pointerInfo.event.clientX;+                        this.lastPointerY = pointerInfo.event.clientY;+                    }+                    // Track middle mouse button+                    if (pointerInfo.event.button === 1) { // Middle mouse button+                        this.isMiddleMouseDown = true;+                    }+                    if (pointerInfo.event.shiftKey && pointerInfo.event.altKey) {+                        // Traditional shift+alt+click behavior (immediate)+                        this.isPointerDownWithShiftAlt = true;+                        const pickResult = pointerInfo.pickInfo;+                        if (pickResult?.hit) {+                            const clickedPoint = pickResult.pickedPoint;+                            if (clickedPoint) {+                                this.handleShiftAltClick(clickedPoint);+                            }+                        }+                    } else if (pointerInfo.event.button === 0 && pointerInfo.event.ctrlKey) {+                        // Ctrl+Left-click: custom panning (matches right-click speed)+                        this.isCtrlLeftPanning = true;+                        this.lastPointerX = pointerInfo.event.clientX;+                        this.lastPointerY = pointerInfo.event.clientY;+                    } else if (pointerInfo.event.button === 0) { // Left mouse button+                        if (this.useClickToSetRotationPoint) {+                            // Store the time when the click started+                            this.clickStartTime = performance.now();+                            // Clear any existing timeout+                            if (this.clickDelayTimeout) {+                                clearTimeout(this.clickDelayTimeout);+                                this.clickDelayTimeout = null;+                            }+                            +                            // Set up a timeout for the delayed click-to-set behavior+                            this.clickDelayTimeout = setTimeout(() => {+                                // Only proceed if we're still in the same click (mouse button still down)+                                if (this.isRotating) {+                                    const pickResult = pointerInfo.pickInfo;+                                    if (pickResult?.hit) {+                                        const clickedPoint = pickResult.pickedPoint;+                                        if (clickedPoint) {+                                            // Handle the delayed click to set rotation point+                                            this.handleClick(clickedPoint);+                                        }+                                    }+                                }+                                this.clickDelayTimeout = null;+                            }, this.clickDelayDuration);+                            +                            // Mark that we're rotating - this will be used for regular rotation until timeout+                            this.isRotating = true;+                            this.showRotationSphere();+                        } else {+                            // Left-click: orbit only (do not set rotation point); show sphere like middle-mouse+                            this.isRotating = true;+                            this.showRotationSphere();+                        }+                    }+                    break;+                case PointerEventTypes.POINTERMOVE:+                    // Custom drag panning (right-click or ctrl+left-click)+                    if (this.isRightMouseDown || this.isCtrlLeftPanning) {+                        const dx = pointerInfo.event.clientX - this.lastPointerX;+                        const dy = pointerInfo.event.clientY - this.lastPointerY;+                        this.lastPointerX = pointerInfo.event.clientX;+                        this.lastPointerY = pointerInfo.event.clientY;++                        // Get camera right and up vectors in world space+                        const rightDir = this.getDirection(Vector3.Right());+                        const upDir = this.getDirection(Vector3.Up());++                        // Scale pan amount by radius so panning feels consistent at any zoom+                        const scale = (this.radius / 1000) * this.panSpeed;+                        const offset = rightDir.scale(-dx * scale).add(upDir.scale(dy * scale));++                        this.target.addInPlace(offset);+                    }+                    if (this.isRotating && !this.isPointerDownWithShiftAlt) {+                        // Keep sphere visible and update its position during rotation+                        // Only update sphere position in normal mode, not in click-to-set mode+                        if (!this.useClickToSetRotationPoint || this.clickDelayTimeout === null) {+                            this.updateRotationSphere();+                        }++                        // Clear any existing timeout+                        if (rotationTimeout) {+                            clearTimeout(rotationTimeout);+                            rotationTimeout = null;+                        }+                    }+                    break;+                case PointerEventTypes.POINTERUP:+                    // Track right mouse button release+                    if (pointerInfo.event.button === 2) {+                        this.isRightMouseDown = false;+                    }+                    // Track ctrl+left-click panning release+                    if (pointerInfo.event.button === 0 && this.isCtrlLeftPanning) {+                        this.isCtrlLeftPanning = false;+                    }+                    // Track middle mouse button release+                    if (pointerInfo.event.button === 1) { // Middle mouse button+                        this.isMiddleMouseDown = false;+                    }+                    +                    if (this.isPointerDownWithShiftAlt) {+                        this.isPointerDownWithShiftAlt = false;+                        this._resetCameraTarget();+                        +                        // Reset middle mouse state for clean detection+                        // console.log("Shift+Alt interaction ended - resetting middle mouse state");+                        this.resetMiddleMouseState();+                    }++                    if (this.isRotating && pointerInfo.event.button === 0) { // Only reset for left mouse button+                        this.isRotating = false;++                        // Cancel the delayed click-to-set behavior on pointer up+                        if (this.clickDelayTimeout) {+                            clearTimeout(this.clickDelayTimeout);+                            this.clickDelayTimeout = null;+                        }++                        // Hide the sphere on mouse up only if it was shown by left-click rotation+                        // Don't hide if we're currently orbiting with spacebar or middle mouse rotating+                        if (this.sphere && !this.isSpacebarPressed && !this.isMiddleMouseRotating) {+                            this.sphere.isVisible = false;+                        }++                        // Clean up any pending timeout+                        if (rotationTimeout) {+                            clearTimeout(rotationTimeout);+                            rotationTimeout = null;+                        }+                        +                        // console.log("Left mouse released - reset isRotating, ready for middle mouse");+                        +                        // Reset middle mouse state for clean detection+                        this.resetMiddleMouseState();+                    }+                    break;+            }+        });+    }++    /**+     * Set up enhanced pinch zoom for trackpad gestures+     * Trackpad pinch sends wheel events with ctrlKey=true and smaller deltaY values+     * Implements zoom-to-cursor like zoomToMouseLocation does for regular wheel+     */+    private setupPinchZoom() {+        if (!this.canvas) return;++        this.wheelHandler = (e: WheelEvent) => {+            // Only handle pinch gestures (ctrlKey is true for trackpad pinch)+            if (e.ctrlKey) {+                e.preventDefault();+                +                // Apply boosted zoom for pinch gestures+                // Negative deltaY = pinch out (zoom in), positive = pinch in (zoom out)+                const zoomDelta = -e.deltaY * this.pinchZoomMultiplier;+                const zoomFactor = 1 + (zoomDelta * 0.01);+                +                // Calculate new radius+                let newRadius = this.radius / zoomFactor;+                +                // Respect radius limits+                if (this.lowerRadiusLimit !== null && newRadius < this.lowerRadiusLimit) {+                    newRadius = this.lowerRadiusLimit;+                } else if (this.upperRadiusLimit !== null && newRadius > this.upperRadiusLimit) {+                    newRadius = this.upperRadiusLimit;+                }+                +                // Zoom to mouse location (like zoomToMouseLocation for regular wheel)+                if (this.zoomToMouseLocation && this._scene) {+                    const radiusChange = this.radius - newRadius;+                    +                    // Get mouse position relative to canvas+                    const rect = this.canvas!.getBoundingClientRect();+                    const mouseX = e.clientX - rect.left;+                    const mouseY = e.clientY - rect.top;+                    +                    // Create a ray from the mouse position+                    const ray = this._scene.createPickingRay(+                        mouseX, +                        mouseY, +                        null, +                        this, +                        false+                    );+                    +                    // In orthographic mode the ray origin is on the near plane (not the camera),+                    // so we must intersect the ray with the plane through the target (perpendicular+                    // to view) to get the correct point under the cursor. In perspective, ray.origin+                    // is the camera position so distanceAlongRay = distanceToTarget is correct.+                    let pointAtTarget: Vector3;+                    if (this.mode === ArcRotateCamera.ORTHOGRAPHIC_CAMERA) {+                        const viewDir = ray.direction;+                        const t = Vector3.Dot(this.target.subtract(ray.origin), viewDir);+                        pointAtTarget = ray.origin.add(viewDir.scale(t));+                    } else {+                        const distanceToTarget = Vector3.Distance(this.position, this.target);+                        pointAtTarget = ray.origin.add(ray.direction.scale(distanceToTarget));+                    }+                    +                    // Calculate how much to move the target toward the mouse point+                    // The amount is proportional to the zoom change+                    const moveRatio = radiusChange / this.radius;+                    const targetOffset = pointAtTarget.subtract(this.target).scale(moveRatio);+                    +                    // Apply the target offset+                    this.target.addInPlace(targetOffset);+                }+                +                // Apply the new radius+                this.radius = newRadius;+            }+        };++        this.canvas.addEventListener('wheel', this.wheelHandler, { passive: false });+    }++    /**+     * Set the pinch zoom sensitivity multiplier+     * Higher values = faster zoom on trackpad pinch+     * @param multiplier The multiplier (default is 3.0, range 1.0-10.0 recommended)+     */+    public setPinchZoomMultiplier(multiplier: number): void {+        this.pinchZoomMultiplier = Math.max(0.5, Math.min(20, multiplier));+    }++    /**+     * Get the current pinch zoom multiplier+     */+    public getPinchZoomMultiplier(): number {+        return this.pinchZoomMultiplier;+    }++    /**+     * Show the rotation sphere at the current target position+     */+    private showRotationSphere() {+        if (!this.isPointerDownWithShiftAlt) {+            this.sphere.position = this.target.clone();+            this.sphere.isVisible = true;+            const scale = (this.radius / 100);+            this.sphere.scaling = new Vector3(scale, scale, scale);+        }+    }++    /**+     * Update the rotation sphere position and scale+     */+    private updateRotationSphere() {+        if (!this.isPointerDownWithShiftAlt && this.sphere) {+            this.sphere.position = this.target.clone();+            const scale = (this.radius / 100);+            this.sphere.scaling = new Vector3(scale, scale, scale);+        }+    }++    /***+    * Move camera to position and add the glowing sphere+    * Show the user the new rotation axis point+    */+    private handleShiftAltClick(clickedPoint: Vector3) {+        const tempRadius = this.radius;++        // Store the previous target+        this.lastCamTarget = this.target.clone();++        // Show the sphere at the clicked point+        this.sphere.position = clickedPoint;+        this.sphere.isVisible = true;+        const scale = (this.radius / 100) * 2;+        this.sphere.scaling = new Vector3(scale, scale, scale);++        // Animate the camera to the clicked point+        this.lastCamTargetMove = this.moveCamTargetTo(clickedPoint._x, clickedPoint._y, clickedPoint._z, 120, 60);++        // Preserve the camera radius+        this.radius = tempRadius;+    }++    /**+     * Handle a mouse click when in click-to-set-rotation mode+     * Similar to handleShiftAltClick but designed for regular clicks+     */+    private handleClick(clickedPoint: Vector3) {+        const tempRadius = this.radius;++        // Store the previous target+        this.lastCamTarget = this.target.clone();++        // Show the sphere at the clicked point+        this.sphere.position = clickedPoint;+        this.sphere.isVisible = true;+        const scale = (this.radius / 100);+        this.sphere.scaling = new Vector3(scale, scale, scale);++        // Animate the camera to the clicked point+        this.lastCamTargetMove = this.moveCamTargetTo(clickedPoint._x, clickedPoint._y, clickedPoint._z, 120, 60);++        // Preserve the camera radius+        this.radius = tempRadius;+    }++    /***+    * Reset the camera's target+    */+    private _resetCameraTarget() {+        // Hide the sphere only if it's not being used for middle mouse rotation or spacebar orbiting+        if (!this.isMiddleMouseRotating && !this.isSpacebarPressed) {+            this.sphere.isVisible = false;+        }++        // Stop and clear the animation if it exists+        if (this.lastCamTargetMove) {+            this.lastCamTargetMove.stop();+            this.lastCamTargetMove = null;+            // console.log("Stopped and cleared camera animation");+        }+    }++    /***+     * Move the camera to its new target+     * @param x :number The x position of the target+     * @param y :number The y position of the target+     * @param z :number The z position of the target+     * @param fps :number frames per second (defaults to 60)+     * @param totalFrames :number total frames of the animation (this/fps = length of animation)+    */+    public moveCamTargetTo(x: number, y: number, z: number, fps: number = 60, totalFrames: number = 60): any {+        // console.log("moving camera");+        const animTarget = new Animation('target', 'target', fps, Animation.ANIMATIONTYPE_VECTOR3, Animation.ANIMATIONLOOPMODE_CONSTANT);+        const easingFunction = new QuadraticEase();+        easingFunction.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT);++        animTarget.setKeys([+            { frame: 0, value: this.target },+            { frame: totalFrames, value: new Vector3(x, y, z) }+        ]);+        this.animations.push(animTarget);++        const animatable = this._scene.beginAnimation(this, 0, totalFrames, false);+        +        // Add a safety timeout to ensure animation reference is cleared+        // Duration = (totalFrames / fps) * 1000ms + 100ms buffer+        const animationDuration = (totalFrames / fps) * 1000 + 100;+        setTimeout(() => {+            if (this.lastCamTargetMove === animatable) {+                // console.log("Safety timeout: clearing animation reference");+                this.lastCamTargetMove = null;+            }+        }, animationDuration);+        +        return animatable;+    }+++    /***+     * Instantly move the camera to this new spot+     * @param x :number The x position of the camera (ignored - derived from other parameters)+     * @param y :number The y position of the camera (ignored - derived from other parameters)+     * @param z :number The z position of the camera (ignored - derived from other parameters)+     * @param tx :number The x position of the camera target+     * @param ty :number The y position of the camera target+     * @param tz :number The z position of the camera target+     * @param r :number The radius of the camera+     * @param a :number The alpha of the camera+     * @param b :number The beta of the camera+    */+    public teleportCamTo(x: number, y: number, z: number, tx: number, ty: number, tz: number, r: number, a: number, b: number) {+        // In an ArcRotateCamera, position is derived from target, radius, alpha, and beta+        // So we don't need to set position directly - it'll be computed automatically+        this.target = new Vector3(tx, ty, tz);+        this.radius = r;+        this.alpha = a;+        this.beta = b;+        +        // The camera will update position automatically+        this.rebuildAnglesAndRadius();+    }++    /***+     * Move the camera slowly to a new position and target with optional easing function.+     * @param x :number The x position of the camera (will be calculated based on target, radius, alpha, beta)+     * @param y :number The y position of the camera (will be calculated based on target, radius, alpha, beta)+     * @param z :number The z position of the camera (will be calculated based on target, radius, alpha, beta)+     * @param tx :number The x position of the camera target+     * @param ty :number The y position of the camera target+     * @param tz :number The z position of the camera target+     * @param r :number The radius of the camera+     * @param a :number The alpha of the camera+     * @param b :number The beta of the camera+     * @param fps :number Optional Defaults to 60. The frames per second of the animation+     * @param totalFrames :number Optional Defaults to 60. The total frames of the animation+     * @param easingFunction :EasingFunction Optional Defaults to QuadraticEase. The easing function to use+    */+    public async moveCamTo(x: number, y: number, z: number, tx: number, ty: number, tz: number,+        r: number, a: number, b: number, fps:number =60, totalFrames:number =60,+        easingFunction = new QuadraticEase()) {++        // For each easing function, you can choose between EASEIN (default), EASEOUT, EASEINOUT+        easingFunction.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT);++        // For ArcRotateCamera, we want to ignore the position input and focus on target, radius, alpha, beta+        // Input position parameters are ignored, since position is derived from target, radius, alpha, and beta+        +        // Animate target+        const animTarget = new Animation('target', 'target', fps, Animation.ANIMATIONTYPE_VECTOR3, Animation.ANIMATIONLOOPMODE_CONSTANT);+        animTarget.setEasingFunction(easingFunction);+        animTarget.setKeys([+            { frame: 0, value: this.target },+            { frame: totalFrames, value: new Vector3(tx, ty, tz) }+        ]);+        this.animations.push(animTarget);++        // Animate radius+        const animRadius = new Animation('radius', 'radius', fps, Animation.ANIMATIONTYPE_FLOAT, Animation.ANIMATIONLOOPMODE_CONSTANT);+        animRadius.setEasingFunction(easingFunction);+        animRadius.setKeys([+            { frame: 0, value: this.radius },+            { frame: totalFrames, value: r }+        ]);+        this.animations.push(animRadius);++        // Animate alpha (horizontal rotation)+        const animAlpha = new Animation('alpha', 'alpha', fps, Animation.ANIMATIONTYPE_FLOAT, Animation.ANIMATIONLOOPMODE_CONSTANT);+        animAlpha.setEasingFunction(easingFunction);+        animAlpha.setKeys([+            { frame: 0, value: this.alpha },+            { frame: totalFrames, value: a }+        ]);+        this.animations.push(animAlpha);++        // Animate beta (vertical rotation)+        const animBeta = new Animation('beta', 'beta', fps, Animation.ANIMATIONTYPE_FLOAT, Animation.ANIMATIONLOOPMODE_CONSTANT);+        animBeta.setEasingFunction(easingFunction);+        animBeta.setKeys([+            { frame: 0, value: this.beta },+            { frame: totalFrames, value: b }+        ]);+        this.animations.push(animBeta);++        if (AdomStatics.scene) {+            // console.log("Started moveCamTo anim");+            const anim = AdomStatics.scene.beginAnimation(this, 0, totalFrames, false);+            await anim.waitAsync();+            // console.log("Finished moveCamTo anim");+        }+    }++    /**+     * Dispose of the camera (for cleanup).+     */+    public dispose() {+        // Stop any running orbit animation+        if (this.orbitAnimationFrameId !== null) {+            cancelAnimationFrame(this.orbitAnimationFrameId);+            this.orbitAnimationFrameId = null;+        }+        +        // Clear any pending click delay timeout+        if (this.clickDelayTimeout) {+            clearTimeout(this.clickDelayTimeout);+            this.clickDelayTimeout = null;+        }+        +        // Clear rotation detection timeout+        if (this.rotationDetectionTimeout) {+            clearTimeout(this.rotationDetectionTimeout);+            this.rotationDetectionTimeout = null;+        }+        +        // Clear key states+        Object.keys(this.keysPressed).forEach(key => {+            this.keysPressed[key] = false;+        });+        +        // Reset mouse states+        this.isMiddleMouseDown = false;+        this.isMiddleMouseRotating = false;+        +        // Remove external event listeners+        if (typeof window !== 'undefined') {+            this.externalEventListeners.forEach((listener, eventName) => {+                window.removeEventListener(eventName, listener);+            });+            this.externalEventListeners.clear();+            // console.log('Camera external event listeners removed');+        }+        +        // Remove pinch zoom wheel handler+        if (this.canvas && this.wheelHandler) {+            this.canvas.removeEventListener('wheel', this.wheelHandler);+            this.wheelHandler = null;+        }+        this.canvas = null;++        if (this.sphere) {+            this.sphere.dispose();+        }+        if (this.sphereHighlightLayer) {+            this.sphereHighlightLayer.dispose();+        }++        super.dispose();+        // console.log("adom camera dispose end");+    }++    /**+     * Set bounds for the camera target+     * Pass null for any bound you don't want to constrain+     */+    public setTargetBounds(+        minX: number | null = null,+        maxX: number | null = null,+        minY: number | null = null,+        maxY: number | null = null,+        minZ: number | null = null,+        maxZ: number | null = null+    ): void {+        this.targetBounds = { minX, maxX, minY, maxY, minZ, maxZ };+        this.enforceBounds = true;++        // Immediately enforce bounds if camera is already outside them+        this.checkAndEnforceBounds();+    }++    /**+     * Disable bounds checking for the camera target+     */+    public disableBounds(): void {+        this.enforceBounds = false;+    }++    /**+     * Check if the camera target is within bounds and correct if needed+     */+    private checkAndEnforceBounds(): void {+        let needsUpdate = false;+        let newTarget = this.target.clone();++        // Check X bounds+        if (this.targetBounds.minX !== null && newTarget.x < this.targetBounds.minX) {+            newTarget.x = this.targetBounds.minX;+            needsUpdate = true;+        }+        if (this.targetBounds.maxX !== null && newTarget.x > this.targetBounds.maxX) {+            newTarget.x = this.targetBounds.maxX;+            needsUpdate = true;+        }++        // Check Y bounds+        if (this.targetBounds.minY !== null && newTarget.y < this.targetBounds.minY) {+            newTarget.y = this.targetBounds.minY;+            needsUpdate = true;+        }+        if (this.targetBounds.maxY !== null && newTarget.y > this.targetBounds.maxY) {+            newTarget.y = this.targetBounds.maxY;+            needsUpdate = true;+        }++        // Check Z bounds+        if (this.targetBounds.minZ !== null && newTarget.z < this.targetBounds.minZ) {+            newTarget.z = this.targetBounds.minZ;+            needsUpdate = true;+        }+        if (this.targetBounds.maxZ !== null && newTarget.z > this.targetBounds.maxZ) {+            newTarget.z = this.targetBounds.maxZ;+            needsUpdate = true;+        }++        // Update the target if it was out of bounds+        if (true) {+            // Hide the sphere first+            const wasSphereVisible = this.sphere && this.sphere.isVisible;+            if (wasSphereVisible) {+                this.sphere.isVisible = false;+            }++            // Update the camera target+            this.setTarget(newTarget);++            // Instantly update the sphere position and make it visible again if it was visible+            if (wasSphereVisible) {+                this.sphere.position = newTarget.clone();+                this.sphere.isVisible = true;+            }+        }+    }++    /**+     * Set whether left-click alone should set rotation point (true) or require shift+alt+click (false)+     * @param useClickMode If true, normal left-clicks set rotation point like shift+alt+click+     * @param delayDuration Optional delay in milliseconds before triggering (defaults to 500ms/0.5sec)+     */+    public setClickToSetRotationPoint(useClickMode: boolean, delayDuration: number = 500): void {+        this.useClickToSetRotationPoint = useClickMode;+        this.clickDelayDuration = delayDuration;+    }++    /**+     * Get current click mode setting+     * @returns True if normal left-clicks set rotation point, false if shift+alt+click is required+     */+    public getClickToSetRotationPoint(): boolean {+        return this.useClickToSetRotationPoint;+    }++    /**+     * Process keyboard input for first-person camera movement+     */+    private processKeyboardInput(): void {+        if (!Object.values(this.keysPressed).some(pressed => pressed)) {+            return; // No keys are pressed, skip processing+        }+        +        // Get camera direction vectors for movement+        // IMPORTANT: For Babylon.js, Vector3.Forward() is actually pointing toward negative Z+        // So we need to reverse it to get the proper forward direction for first-person movement+        const forward = this.getDirection(new Vector3(0, 0, -1)); // True forward in camera space+        const right = this.getDirection(Vector3.Right());         // Right in camera space+        const up = this.upVector;                                // Global up vector+        +        // For true FPS-style movement, we want movement in the horizontal plane+        // So we zero out the Z component and renormalize+        const horizontalForward = new Vector3(forward.x, forward.y, 0).normalize();+        const horizontalRight = new Vector3(right.x, right.y, 0).normalize();+        +        // Create move direction vector based on keys pressed+        const moveDirection = new Vector3(0, 0, 0);+        +        // Correct W/S logic: +        // W should move forward along horizontalForward+        // S should move backward (opposite of horizontalForward)+        if (this.keysPressed['w']) {+            // W = move forward in the direction we're facing+            moveDirection.addInPlace(horizontalForward);+        }+        if (this.keysPressed['s']) {+            // S = move backward, opposite of the direction we're facing+            moveDirection.addInPlace(horizontalForward.scale(-1));+        }+        if (this.keysPressed['a']) {+            // A = strafe left (opposite of right)+            moveDirection.addInPlace(horizontalRight.scale(-1));+        }+        if (this.keysPressed['d']) {+            // D = strafe right+            moveDirection.addInPlace(horizontalRight);+        }+        if (this.keysPressed['q']) {+            // Q = move down+            moveDirection.addInPlace(up.scale(-1));+        }+        if (this.keysPressed['e']) {+            // E = move up+            moveDirection.addInPlace(up);+        }+        +        // Normalize the move direction if we have any movement+        if (moveDirection.length() > 0) {+            moveDirection.normalize();+            +            // Scale by move speed, proportional to camera radius so tiny/huge models feel the same+            const scaledSpeed = this.moveSpeed * (this.radius / 100);+            const movement = moveDirection.scale(scaledSpeed);+            +            // Move both camera position and target to maintain orientation+            this.position.addInPlace(movement);+            this.target.addInPlace(movement);+            +            // Update orbit sphere position if visible+            if (this.sphere && this.sphere.isVisible) {+                this.sphere.position = this.target.clone();+            }+        }+    }+    +    /**+     * Set the movement speed for first-person controls+     */+    public setMoveSpeed(speed: number): void {+        this.baseMoveSpeed = speed;+        // Apply precision mode slowdown if active+        this.moveSpeed = this.precisionMode ? speed / 4 : speed;+    }+    +    /**+     * Get the current movement speed+     */+    public getMoveSpeed(): number {+        return this.moveSpeed;+    }+    +    /**+     * Rotate camera to a specific viewcube angle+     * @param alpha The target alpha angle+     * @param beta The target beta angle+     * @param duration Animation duration in seconds (defaults to 0.5)+     */+    public rotateToCubeAngle(alpha: number, beta: number, duration: number = 0.5): void {+        // Calculate shortest path for alpha rotation+        const normalizeAngle = (angle: number): number => {+            return angle - Math.PI * 2 * Math.floor((angle + Math.PI) / (2 * Math.PI));+        };+        +        const shortestAngleDistance = (from: number, to: number): number => {+            const normalizedFrom = normalizeAngle(from);+            const normalizedTo = normalizeAngle(to);+            const difference = normalizedTo - normalizedFrom;+            if (difference > Math.PI) {+                return difference - Math.PI * 2;+            } else if (difference < -Math.PI) {+                return difference + Math.PI * 2;+            }+            return difference;+        };+        +        const targetAlpha = this.alpha + shortestAngleDistance(this.alpha, alpha);+        const targetBeta = this.beta + shortestAngleDistance(this.beta, beta);++        if (duration <= 0) {+            this.alpha = targetAlpha;+            this.beta = targetBeta;+            return;+        }++        const fps = 60;+        const totalFrames = fps * duration;+        +        // Create alpha animation+        const alphaAnimation = new Animation(+            'alphaAnimation',+            'alpha',+            fps,+            Animation.ANIMATIONTYPE_FLOAT,+            Animation.ANIMATIONLOOPMODE_CONSTANT+        );+        alphaAnimation.setKeys([+            { frame: 0, value: this.alpha },+            { frame: totalFrames, value: targetAlpha }+        ]);+        +        // Create beta animation+        const betaAnimation = new Animation(+            'betaAnimation',+            'beta',+            fps,+            Animation.ANIMATIONTYPE_FLOAT,+            Animation.ANIMATIONLOOPMODE_CONSTANT+        );+        betaAnimation.setKeys([+            { frame: 0, value: this.beta },+            { frame: totalFrames, value: targetBeta }+        ]);+        +        this.animations = [alphaAnimation, betaAnimation];+        this._scene.beginAnimation(this, 0, totalFrames, false);+    }+    +    /**+     * Rotate camera to top view+     */+    public rotateToCubeTop(duration: number = 0.5): void {+        // Top face: position [0, 0, cr] where cr = 2+        // For top view: looking straight down+        const alpha = this.alpha; // Keep current alpha (rotation around Z)+        const beta = 0; // Looking straight down from top+        this.rotateToCubeAngle(alpha, beta, duration);+    }+    +    /**+     * Rotate camera to top-front-right corner view+     */+    public rotateToCubeTopFrontRight(duration: number = 0.5): void {+        // Top-front-right corner: position [cr-es/2, -(cr-es/2), cr-es/2]+        // With cr=2, es=0.75: position ≈ [1.625, -1.625, 1.625]+        const x = 1.625, y = -1.625, z = 1.625;+        const adjust = -Math.PI / 2;+        const alpha = Math.atan2(x, y) + adjust;+        const beta = Math.acos(z / Math.sqrt(x * x + y * y + z * z));+        this.rotateToCubeAngle(alpha, beta, duration);+    }+    +    /**+     * Rotate camera to top-front-left corner view+     */+    public rotateToCubeTopFrontLeft(duration: number = 0.5): void {+        // Top-front-left corner: position [-(cr-es/2), -(cr-es/2), cr-es/2]+        // With cr=2, es=0.75: position ≈ [-1.625, -1.625, 1.625]+        const x = -1.625, y = -1.625, z = 1.625;+        const adjust = -Math.PI / 2;+        const alpha = Math.atan2(x, y) + adjust;+        const beta = Math.acos(z / Math.sqrt(x * x + y * y + z * z));+        this.rotateToCubeAngle(alpha, beta, duration);+    }+    +    /**+     * Rotate camera to top-back-right corner view+     */+    public rotateToCubeTopBackRight(duration: number = 0.5): void {+        // Top-back-right corner: position [cr-es/2, cr-es/2, cr-es/2]+        // With cr=2, es=0.75: position ≈ [1.625, 1.625, 1.625]+        const x = 1.625, y = 1.625, z = 1.625;+        const adjust = -Math.PI / 2;+        const alpha = Math.atan2(x, y) + adjust;+        const beta = Math.acos(z / Math.sqrt(x * x + y * y + z * z));+        this.rotateToCubeAngle(alpha, beta, duration);+    }+    +    /**+     * Rotate camera to top-back-left corner view+     */+    public rotateToCubeTopBackLeft(duration: number = 0.5): void {+        // Top-back-left corner: position [-(cr-es/2), cr-es/2, cr-es/2]+        // With cr=2, es=0.75: position ≈ [-1.625, 1.625, 1.625]+        const x = -1.625, y = 1.625, z = 1.625;+        const adjust = -Math.PI / 2;+        const alpha = Math.atan2(x, y) + adjust;+        const beta = Math.acos(z / Math.sqrt(x * x + y * y + z * z));+        this.rotateToCubeAngle(alpha, beta, duration);+    }+    +    /**+     * Toggle between orthographic and perspective camera modes.+     * When linked to the moving camera, switching to orthographic is blocked.+     * @returns The new camera mode (ORTHOGRAPHIC_CAMERA or PERSPECTIVE_CAMERA)+     */+    public toggleCameraMode(): number {+        if (this.mode === ArcRotateCamera.ORTHOGRAPHIC_CAMERA) {+            this.mode = ArcRotateCamera.PERSPECTIVE_CAMERA;+            // console.log('Switched to Perspective Camera');+        } else {+            this.mode = ArcRotateCamera.ORTHOGRAPHIC_CAMERA;+            // Update orthographic bounds when switching to ortho mode+            this.updateOrthographicBounds();+            // console.log('Switched to Orthographic Camera');+        }+        return this.mode;+    }+    +    /**+     * Get the current camera mode+     * @returns The current camera mode (ORTHOGRAPHIC_CAMERA or PERSPECTIVE_CAMERA)+     */+    public getCameraMode(): number {+        return this.mode;+    }+    +    /**+     * Check if camera is in orthographic mode+     * @returns True if orthographic, false if perspective+     */+    public isOrthographic(): boolean {+        return this.mode === ArcRotateCamera.ORTHOGRAPHIC_CAMERA;+    }+}
vendor/adom-step/vendor/adom-3d-viewer-babylon9/src/lib/adom-babylon/assets/adomLogo.tsadded
@@ -0,0 +1 @@