Merge #a9165a5e: 3D hardware models, NIP-99 discovery, and private phon…
Demo images / Build & push demo images (push) Failing after 41s
3D hardware models, NIP-99 discovery, and private phone goal nostr:nevent1qqs2j9j6tc3kpsdp3jhwty3wt8yy3un7r93e4d4smjpza4nvt033ctgpz3mhxue69uhhyetvv9ujumn8d96zuer9wcq28amm PR-Author: Personal nostr:npub1w3sqdkrhn0gyuvsex32effzgnfpyde6qrrc4u467flg5e9txh4wsfn5vjg PR description: Adds refined 3D hardware illustrations, OpenWrt and private-phone marketplace discovery, and the Setup goal for a non-surveillance phone. Production queries enabled Archipelago relays for NIP-99 kind-30402 listings; development mock cards are used only when DEV is enabled. Tested with production build plus Chromium, Firefox, and WebKit desktop/mobile browser checks. File-browser goal completion now opens the native Cloud files view.
@@ -0,0 +1,93 @@
|
||||
# Interactive hardware illustrations
|
||||
|
||||
The radio detection modal and unconfigured OpenWrt gateway use original
|
||||
procedural Three.js models. The renderer follows the studio lighting,
|
||||
rounded geometry, material finishes and instancing approach used for the
|
||||
computer illustrations in `archipelago-website-deck`.
|
||||
|
||||
## Coverage
|
||||
|
||||
`neode-ui/src/graphics/deviceModels.ts` contains 25 radio illustrations,
|
||||
including the generic fallback and three optional enclosure variants, plus
|
||||
the Beryl AX (GL-MT3000) and a black Pixel 9 Pro. All existing `meshDeviceImages.ts` detection results
|
||||
resolve to a procedural model key. USB chip identification remains a
|
||||
best-effort visual match; it does not select firmware or claim exact hardware.
|
||||
|
||||
The radios include modeled rear faces, enclosure panels or populated boards,
|
||||
and appropriate battery holders, connectors and antennas. These are stylized
|
||||
recognition illustrations, not CAD models. Fine component placement and rear
|
||||
details are representative; not every board revision or enclosure has been
|
||||
verified against a rear photograph. The Beryl AX is the example router from
|
||||
`docs/openwrt-gateway-setup.md`, not an assertion about the connected router.
|
||||
|
||||
Reference material:
|
||||
|
||||
- Beryl AX official front/rear/side hardware drawing:
|
||||
https://static.gl-inet.com/docs/router/en/4/user_guide/gl-mt3000/hardware_info/mt3000_interface.jpg
|
||||
- Beryl AX guide:
|
||||
https://docs.gl-inet.com/router/en/4/user_guide/gl-mt3000/
|
||||
- T-Echo manufacturer board and enclosure reference:
|
||||
https://github.com/Xinyuan-LilyGO/T-Echo
|
||||
https://raw.githubusercontent.com/Xinyuan-LilyGO/T-Echo/main/image/T-ECHO.jpg
|
||||
- T-Deck manufacturer documentation:
|
||||
https://wiki.lilygo.cc/products/t-deck-series/t-deck/
|
||||
|
||||
Manufacturer photographs are references only; none are copied into the app.
|
||||
Geometry, grain and device labels are generated locally without CDN assets.
|
||||
The black Pixel 9 Pro illustration uses an unmodified, locally bundled GrapheneOS
|
||||
boot logo mask from its framework source. Its rounded outline, glass panels,
|
||||
camera island, layered lenses, side buttons and ports are procedural geometry.
|
||||
The display shows a static phase of the OS boot animation, distinct from Google's
|
||||
bootloader notice. Source and attribution: `neode-ui/src/graphics/assets/ATTRIBUTION.md`.
|
||||
|
||||
## Interaction and lifecycle
|
||||
|
||||
`DeviceModel.vue` loads the renderer on intersection. It turns once per minute,
|
||||
capped at 30 rendered frames per second. Drag horizontally to rotate; a mouse
|
||||
can also tilt vertically. Touch preserves vertical page scrolling. Arrow keys
|
||||
rotate/tilt, Home shows the front and End shows the back. Keyboard interaction
|
||||
pauses automatic turning. The Pause/Rotate button controls autoplay.
|
||||
|
||||
Reduced motion disables autoplay and hides its control while preserving manual
|
||||
rotation. Hidden tabs and offscreen models stop scheduling frames. Model changes
|
||||
reuse the renderer/environment and dispose the previous geometry, textures and
|
||||
materials. Unmount releases the renderer and context. Unsupported browser APIs,
|
||||
unavailable WebGL, import failures or lost contexts leave a static fallback.
|
||||
|
||||
The router figure uses the remaining viewport height below the connection form
|
||||
on desktop (minimum 260px); mobile layouts get a fixed 290px illustration. Small desktop
|
||||
windows can scroll normally rather than collapsing the model to zero height.
|
||||
|
||||
## Hardware discovery
|
||||
|
||||
The router gateway and private-phone Setup goal share `HardwareMarketplace.vue`.
|
||||
Buy router / Buy phone replaces the model with a horizontal product carousel,
|
||||
preserving the outer container's height. Discovery queries enabled relays from
|
||||
`nostr.list-relays` for NIP-99 kind-30402 events. Phone discovery uses the `pixel`
|
||||
keyword. Development builds show clearly
|
||||
labeled sample listings when no matching live listings are found. Checkout and
|
||||
the product View/Buy actions remain unimplemented.
|
||||
|
||||
Browser scenarios and reports stay outside this repository, under
|
||||
`/home/yaya/.local/share/browser-check/projects/archy-devices/`.
|
||||
|
||||
## Validation (2026-10-09)
|
||||
|
||||
- Production typecheck/build passes; the lazy renderer chunk is 136 KB gzipped.
|
||||
- Existing gateway/cache tests (10) and radio setup tests (2) pass.
|
||||
- Chromium 153, Firefox 155 and WebKit 26.6 pass at desktop 1440×900 and
|
||||
phone 390×844: all 26 models, front/back rotation, drag, autoplay/pause,
|
||||
reduced motion, zero offscreen/idle draws, model replacement, unmount/remount
|
||||
and context-loss fallback. Reports: `/tmp/archy-models-final/`.
|
||||
- Actual gateway and detection-modal components pass the same six-case matrix
|
||||
with mocked RPC data. Reports: `/tmp/archy-device-pages-matrix/`.
|
||||
- Delayed renderer loading, storage denial, reload and unavailable WebGL pass
|
||||
the same matrix. Reports: `/tmp/archy-device-fallbacks/`.
|
||||
- These are Linux browser tests with phone viewport emulation, not physical
|
||||
iPhone/Android GPU or touch-scroll certification. No live router was changed.
|
||||
|
||||
Branch: `feat/3d-mesh-router`, based on ngit main
|
||||
`5812f53c7c05027848a1007f818e9d33117b62e0`. The ngit helper could read relay
|
||||
state but failed Git server authentication; main's published kind-30618 ref
|
||||
was checked on relay.ngit.dev and that exact commit fetched from the existing
|
||||
Git mirror. No remote proposal, merge or deployment was performed.
|
||||
@@ -33,6 +33,8 @@ planned (see docs/LICENSE-COMPLIANCE-AUDIT.md).
|
||||
|
||||
## Vendored
|
||||
|
||||
- `src/graphics/assets/grapheneos-boot-mask.png` — GrapheneOS boot artwork,
|
||||
Apache-2.0; see the adjacent ATTRIBUTION.md for the pinned source and credits.
|
||||
- `public/assets/icon/` — see ATTRIBUTION.md in that directory
|
||||
(game-icons.net CC BY 3.0; pixelarticons MIT).
|
||||
- `public/assets/img/mesh-devices/` — Meshtastic project artwork, GPL-3.0;
|
||||
|
||||
@@ -4470,6 +4470,12 @@ app.post('/rpc/v1', (req, res) => {
|
||||
|
||||
// ── OpenWRT / TollGate gateway (demo: a thoroughly-used gateway) ──
|
||||
case 'openwrt.get-status': {
|
||||
// Keep the local preview in the unconnected state so the router model
|
||||
// is immediately visible. Set MOCK_OPENWRT_CONNECTED=1 to preview the
|
||||
// connected dashboard instead.
|
||||
if (!process.env.MOCK_OPENWRT_CONNECTED) {
|
||||
return res.json({ error: { code: -32004, message: 'No router configured' } })
|
||||
}
|
||||
return res.json({
|
||||
result: {
|
||||
host: '192.168.8.1',
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"qrloop": "^1.4.1",
|
||||
"three": "^0.185.0",
|
||||
"vue": "^3.5.24",
|
||||
"vue-i18n": "^11.3.0",
|
||||
"vue-router": "^4.6.3"
|
||||
@@ -32,6 +33,7 @@
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/three": "^0.185.0",
|
||||
"@vite-pwa/assets-generator": "^1.0.2",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitest/coverage-v8": "^5.0.3",
|
||||
@@ -1825,6 +1827,13 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@dimforge/rapier3d-compat": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
||||
@@ -3753,6 +3762,13 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "23.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||
@@ -4121,12 +4137,41 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/stats.js": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/three": {
|
||||
"version": "0.185.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.0.tgz",
|
||||
"integrity": "sha512-O2Uy8Cj4Nonr8dWUUbifMdPe8B0Mq7EdOHb89S4+kjUw/KhbjTZrUuYlrQ1bpUKG+EP9QJnN7qNxbHGlGoLHMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
"@types/stats.js": "*",
|
||||
"@types/webxr": ">=0.5.17",
|
||||
"fflate": "~0.8.2",
|
||||
"meshoptimizer": "~1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/webxr": {
|
||||
"version": "0.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
|
||||
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vite-pwa/assets-generator": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vite-pwa/assets-generator/-/assets-generator-1.0.4.tgz",
|
||||
@@ -6948,6 +6993,13 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
|
||||
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
|
||||
@@ -8465,6 +8517,13 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/meshoptimizer": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
|
||||
"integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
@@ -10823,6 +10882,12 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.185.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.185.0.tgz",
|
||||
"integrity": "sha512-+yRrcRO2iZa8uzvNNl0d7cL4huhgKgBvVJ0njcTe8xFqZ6DMAFZdCKDP91SEAuj25bNAj7k1QQdf+srZywVK6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.1.tgz",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"qrloop": "^1.4.1",
|
||||
"three": "^0.185.0",
|
||||
"vue": "^3.5.24",
|
||||
"vue-i18n": "^11.3.0",
|
||||
"vue-router": "^4.6.3"
|
||||
@@ -49,6 +50,7 @@
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/three": "^0.185.0",
|
||||
"@vite-pwa/assets-generator": "^1.0.2",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitest/coverage-v8": "^5.0.3",
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# Device Artwork Attribution
|
||||
|
||||
The device illustrations in this directory are from the
|
||||
**Meshtastic® project** — https://meshtastic.org
|
||||
(https://github.com/meshtastic), © Meshtastic contributors,
|
||||
licensed under GPL-3.0.
|
||||
|
||||
Meshtastic® is a registered trademark of Meshtastic LLC, used here solely to
|
||||
identify compatible hardware. No endorsement by the Meshtastic project is
|
||||
implied.
|
||||
|
||||
Any modifications to these images made for Archipelago are likewise available
|
||||
under GPL-3.0 as part of this public repository.
|
||||
|
Before Width: | Height: | Size: 96 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="795.27 277.13 409.46 1319.35"><defs><style>.cls-1{fill:#353535;}.cls-2{fill:#1e1e1d;}.cls-3{fill:#b1a368;}.cls-10,.cls-11,.cls-4,.cls-6,.cls-8,.cls-9{fill:none;}.cls-4,.cls-6{stroke:#050606;}.cls-10,.cls-11,.cls-4,.cls-6,.cls-8{stroke-miterlimit:10;}.cls-4{stroke-width:2.41px;}.cls-5{fill:#30c2db;}.cls-6{stroke-width:3.91px;}.cls-7{fill:#dcf0f2;}.cls-10,.cls-11,.cls-8{stroke:#dcf0f2;}.cls-8{stroke-width:1.81px;}.cls-9{stroke:#17afbf;stroke-linecap:round;stroke-linejoin:round;stroke-width:7.23px;}.cls-10{stroke-width:1.78px;}.cls-11{stroke-width:1.81px;}</style></defs><g id="Layer_7" data-name="Layer 7"><path class="cls-1" fill="#353535" d="M915.62,278.34h22.61a35,35,0,0,1,35,35V715.74a0,0,0,0,1,0,0H880.6a0,0,0,0,1,0,0V313.36A35,35,0,0,1,915.62,278.34Z"></path><rect class="cls-2" fill="#1e1e1d" x="880.6" y="340.15" width="92.65" height="7.54"></rect><rect class="cls-2" fill="#1e1e1d" x="880.6" y="356.68" width="92.65" height="7.54"></rect><rect class="cls-3" fill="#b1a368" x="885.8" y="844.3" width="84.14" height="19.02"></rect><rect class="cls-3" fill="#b1a368" x="880.6" y="819.07" width="92.65" height="25.23"></rect><rect class="cls-3" fill="#b1a368" x="885.8" y="790.65" width="84.14" height="28.41"></rect><rect class="cls-3" fill="#b1a368" x="880.6" y="723.02" width="92.65" height="67.63"></rect><rect class="cls-3" fill="#b1a368" x="885.8" y="715.74" width="84.14" height="7.28"></rect><rect class="cls-4" stroke-width="2.41px" x="885.8" y="844.3" width="84.14" height="19.02"></rect><rect class="cls-4" stroke-width="2.41px" x="880.6" y="819.07" width="92.65" height="25.23"></rect><rect class="cls-4" stroke-width="2.41px" x="885.8" y="790.65" width="84.14" height="28.41"></rect><rect class="cls-4" stroke-width="2.41px" x="880.6" y="723.02" width="92.65" height="67.63"></rect><rect class="cls-4" stroke-width="2.41px" x="885.8" y="715.74" width="84.14" height="7.28"></rect><path class="cls-4" stroke-width="2.41px" d="M915.62,278.34h22.61a35,35,0,0,1,35,35V715.74a0,0,0,0,1,0,0H880.6a0,0,0,0,1,0,0V313.36A35,35,0,0,1,915.62,278.34Z"></path><rect class="cls-4" stroke-width="2.41px" x="880.6" y="340.15" width="92.65" height="7.54"></rect><rect class="cls-4" stroke-width="2.41px" x="880.6" y="356.68" width="92.65" height="7.54"></rect><rect class="cls-5" fill="#30c2db" x="796.48" y="856.3" width="407.05" height="738.98" rx="47.74"></rect><rect class="cls-1" fill="#353535" x="900.05" y="973.19" width="202.03" height="354.65" rx="16.4"></rect><rect class="cls-6" stroke-width="3.91px" x="900.05" y="973.19" width="202.03" height="354.65" rx="16.4"></rect><rect class="cls-7" fill="#dcf0f2" x="871.51" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-7" fill="#dcf0f2" x="1070.16" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-4" stroke-width="2.41px" x="871.51" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-4" stroke-width="2.41px" x="1070.16" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><circle class="cls-8" stroke-width="1.81px" cx="841.7" cy="1537.01" r="16.25"></circle><circle class="cls-8" stroke-width="1.81px" cx="841.7" cy="913.26" r="16.25"></circle><circle class="cls-8" stroke-width="1.81px" cx="1157.32" cy="913.26" r="16.25"></circle><circle class="cls-8" stroke-width="1.81px" cx="1157.32" cy="1504.51" r="16.25"></circle><line class="cls-9" stroke="#17afbf" stroke-width="7.23px" stroke-linecap="round" stroke-linejoin="round" x1="942.51" y1="1592.42" x2="942.51" y2="1381.55"></line><line class="cls-9" stroke="#17afbf" stroke-width="7.23px" stroke-linecap="round" stroke-linejoin="round" x1="966.52" y1="1592.42" x2="966.52" y2="1381.55"></line><line class="cls-9" stroke="#17afbf" stroke-width="7.23px" stroke-linecap="round" stroke-linejoin="round" x1="990.57" y1="1592.42" x2="990.57" y2="1381.55"></line><line class="cls-9" stroke="#17afbf" stroke-width="7.23px" stroke-linecap="round" stroke-linejoin="round" x1="1014.59" y1="1592.42" x2="1014.59" y2="1381.55"></line><line class="cls-9" stroke="#17afbf" stroke-width="7.23px" stroke-linecap="round" stroke-linejoin="round" x1="1038.63" y1="1592.42" x2="1038.63" y2="1381.55"></line><line class="cls-9" stroke="#17afbf" stroke-width="7.23px" stroke-linecap="round" stroke-linejoin="round" x1="1062.65" y1="1592.42" x2="1062.65" y2="1381.55"></line><rect class="cls-4" stroke-width="2.41px" x="796.48" y="856.3" width="407.05" height="738.98" rx="47.74"></rect><path class="cls-10" stroke-width="1.78px" d="M1040.1,947.74H960.65A13.93,13.93,0,0,1,947,936.64l-10.23-49.2a13.93,13.93,0,0,1,13.64-16.77h97.72a13.93,13.93,0,0,1,13.75,16.18l-8,49.2A13.94,13.94,0,0,1,1040.1,947.74Z"></path><rect class="cls-11" stroke-width="1.81px" x="816.35" y="870.67" width="365.51" height="703.12" rx="32.37"></rect><rect class="cls-11" stroke-width="1.81px" x="888.77" y="963.84" width="223.2" height="374.66" rx="25.21"></rect></g></svg>
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 10 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="404.68 390.65 1217.15 959.26"><defs><style>.cls-1{fill:#dfeaf7;}.cls-2{fill:#17907f;}.cls-3{fill:#2b2b2b;}.cls-4,.cls-5{fill:none;stroke:#050606;stroke-miterlimit:10;}.cls-4{stroke-width:2.25px;}.cls-5{stroke-width:4px;}.cls-6{fill:#050606;}</style></defs><g id="Layer_5" data-name="Layer 5"><path class="cls-1" fill="#dfeaf7" d="M1517.73,392.65h0a102.1,102.1,0,0,0-102.1,102.1V770.37A40.62,40.62,0,0,1,1375,811H455.16a48.49,48.49,0,0,0-48.48,48.48v126a11.85,11.85,0,0,0,3.46,8.37l15.34,15.34a11.81,11.81,0,0,1,3.47,8.37v137.16a11.81,11.81,0,0,1-3.47,8.37l-15.34,15.34a11.85,11.85,0,0,0-3.46,8.37v112.67a48.49,48.49,0,0,0,48.48,48.49H1571.34a48.51,48.51,0,0,0,48.49-48.5V494.75A102.1,102.1,0,0,0,1517.73,392.65Zm-110.61,815V954a33.14,33.14,0,0,1,66.27,0v253.65a33.14,33.14,0,0,1-66.27,0Z"></path><path class="cls-2" fill="#17907f" d="M1516,439.16c-30.23.91-53.92,26.54-53.92,56.79V770.37A87.11,87.11,0,0,1,1375,857.48H732.31A27.51,27.51,0,0,0,704.8,885v388.93a27.51,27.51,0,0,0,27.51,27.51h828.38a12.7,12.7,0,0,0,12.65-12.65v-794A55.69,55.69,0,0,0,1516,439.16Zm-108.9,768.47V954a33.14,33.14,0,0,1,66.27,0v253.65a33.14,33.14,0,0,1-66.27,0Z"></path><rect class="cls-3" fill="#2b2b2b" x="787.14" y="943.38" width="429.45" height="224.42"></rect><path class="cls-1" fill="#dfeaf7" d="M1478.6,915.35A54.23,54.23,0,0,0,1386,953.69v254.23a54.23,54.23,0,1,0,108.45,0V953.69A54,54,0,0,0,1478.6,915.35Zm-5.21,292.28a33.14,33.14,0,0,1-66.27,0V954a33.14,33.14,0,0,1,66.27,0Z"></path></g><g id="Layer_2" data-name="Layer 2"><path class="cls-4" stroke-width="2.25px" d="M1573.34,494.75v794a12.68,12.68,0,0,1-12.65,12.65H732.31a27.51,27.51,0,0,1-27.51-27.51V885a27.51,27.51,0,0,1,27.51-27.51H1375a87.11,87.11,0,0,0,87.11-87.11V496c0-30.25,23.69-55.88,53.92-56.79A55.69,55.69,0,0,1,1573.34,494.75Z"></path><path class="cls-5" stroke-width="4px" d="M410.14,1178.39,425.49,1163a11.78,11.78,0,0,0,3.46-8.35V1017.5a11.8,11.8,0,0,0-3.46-8.35l-15.35-15.36a11.77,11.77,0,0,1-3.46-8.35v-126A48.47,48.47,0,0,1,455.16,811H1375a40.63,40.63,0,0,0,40.63-40.63V494.75a102.1,102.1,0,0,1,102.1-102.1h0a102.1,102.1,0,0,1,102.1,102.1v804.66a48.51,48.51,0,0,1-48.49,48.5H455.16a48.48,48.48,0,0,1-48.48-48.49V1186.74A11.78,11.78,0,0,1,410.14,1178.39Z"></path><rect class="cls-4" stroke-width="2.25px" x="1407.12" y="920.85" width="66.26" height="319.9" rx="33.13"></rect><rect class="cls-4" stroke-width="2.25px" x="1386.03" y="899.46" width="108.46" height="362.69" rx="54.23"></rect><path class="cls-6" fill="#050606" d="M639.76,1070.55a2.91,2.91,0,0,1-2.91-2.91v-30.53a5.42,5.42,0,0,0-1.6-3.86l-32.44-32.44a11.86,11.86,0,0,1-3.5-8.44V901a12.52,12.52,0,0,0-12.51-12.51H483.92a12.7,12.7,0,0,0-12.68,12.69v76.78a24.13,24.13,0,0,0,7.11,17.18l14.33,14.33a24.13,24.13,0,0,0,17.18,7.11h50.75a11.86,11.86,0,0,1,8.44,3.5l24.26,24.26a12.47,12.47,0,0,1,3.68,8.88v14.46a2.91,2.91,0,1,1-5.81,0v-14.46a6.72,6.72,0,0,0-2-4.77l-24.26-24.26a6.09,6.09,0,0,0-4.33-1.8H509.86a29.91,29.91,0,0,1-21.29-8.81l-14.33-14.33a29.87,29.87,0,0,1-8.81-21.29V901.14a18.51,18.51,0,0,1,18.49-18.5H586.8A18.34,18.34,0,0,1,605.12,901v91.41a6.09,6.09,0,0,0,1.8,4.33l32.44,32.44a11.19,11.19,0,0,1,3.3,8v30.53A2.9,2.9,0,0,1,639.76,1070.55Z"></path><path class="cls-6" fill="#050606" d="M586.8,1289.46H483.92a18.51,18.51,0,0,1-18.49-18.5v-76.78a29.87,29.87,0,0,1,8.81-21.29l14.33-14.34a29.91,29.91,0,0,1,21.29-8.81h50.75a6.09,6.09,0,0,0,4.33-1.8l24.26-24.25a6.74,6.74,0,0,0,2-4.78v-14.46a2.91,2.91,0,0,1,5.81,0v14.46a12.51,12.51,0,0,1-3.68,8.89l-24.26,24.25a11.86,11.86,0,0,1-8.44,3.5H509.86a24.17,24.17,0,0,0-17.18,7.11L478.35,1177a24.09,24.09,0,0,0-7.11,17.18V1271a12.71,12.71,0,0,0,12.68,12.69H586.8a12.53,12.53,0,0,0,12.51-12.52v-91.4a11.83,11.83,0,0,1,3.5-8.44l32.44-32.44a5.46,5.46,0,0,0,1.6-3.87v-30.53a2.91,2.91,0,0,1,5.81,0V1135a11.19,11.19,0,0,1-3.3,8l-32.44,32.45a6.06,6.06,0,0,0-1.8,4.33v91.4A18.35,18.35,0,0,1,586.8,1289.46Z"></path><rect class="cls-4" stroke-width="2.25px" x="787.14" y="943.38" width="429.45" height="224.42"></rect></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
@@ -1,502 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 293.56501 470.885"
|
||||
version="1.1"
|
||||
id="svg207"
|
||||
sodipodi:docname="buyer benmeshtastic 2-03.svg"
|
||||
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
||||
width="293.565"
|
||||
height="470.88501"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview207"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96807266"
|
||||
inkscape:cx="186.96944"
|
||||
inkscape:cy="370.32344"
|
||||
inkscape:window-width="1728"
|
||||
inkscape:window-height="1056"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="33"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_2" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<style
|
||||
id="style1">.cls-1{fill:#474746;}.cls-2{fill:#3a3a3a;}.cls-3{fill:#202121;}.cls-4{fill:#dbdcdd;}.cls-5{fill:#cbcbcc;}.cls-11,.cls-16,.cls-18,.cls-6{fill:none;}.cls-11,.cls-16,.cls-6{stroke:#1d1d1b;stroke-linecap:round;stroke-linejoin:round;}.cls-6{stroke-width:0.95px;}.cls-7{fill:#e5e2e5;}.cls-8{fill:#444e85;}.cls-21,.cls-22,.cls-9{fill:#1d1d1b;}.cls-10{fill:#d1b066;}.cls-11{stroke-width:1.3px;}.cls-12{fill:#d8b258;}.cls-13{fill:#b28e45;}.cls-14{fill:#a6a9af;}.cls-15{fill:#d1c854;}.cls-16{stroke-width:1.01px;}.cls-17{fill:#fff;}.cls-18{stroke:#fff;stroke-miterlimit:10;}.cls-19{fill:#ba9c61;}.cls-20{fill:#715c34;}.cls-21{font-size:13.29px;}.cls-21,.cls-22{font-family:LucidaConsole, Lucida Console;}.cls-22{font-size:8.44px;}</style>
|
||||
</defs>
|
||||
<g
|
||||
id="Layer_2"
|
||||
data-name="Layer 2"
|
||||
transform="translate(-106.20269,-157.045)">
|
||||
<path
|
||||
class="cls-1" fill="#474746"
|
||||
d="m 396.14,293.93 v 311.36 a 22,22 0 0 1 -22,22 H 128.65 a 22,22 0 0 1 -22,-22 v -225.7 a 22,22 0 0 1 22,-22 h 69.14 a 22,22 0 0 0 22,-22 v -41.6 a 22,22 0 0 1 22,-22 h 132.32 a 22,22 0 0 1 22.03,21.94 z"
|
||||
id="path1" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="m 358.92,384 a 23.67,23.67 0 0 1 23.64,23.64 v 176.6 a 23.67,23.67 0 0 1 -23.64,23.64 H 147.31 A 23.67,23.67 0 0 1 123.67,584.24 V 407.61 A 23.67,23.67 0 0 1 147.31,384 h 211.61 m 0,-5 H 147.31 a 28.64,28.64 0 0 0 -28.64,28.64 v 176.6 a 28.65,28.65 0 0 0 28.64,28.64 h 211.61 a 28.64,28.64 0 0 0 28.64,-28.64 V 407.61 A 28.64,28.64 0 0 0 358.92,379 Z"
|
||||
id="path2" />
|
||||
<rect
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
x="237.61"
|
||||
y="284.09"
|
||||
width="139.52"
|
||||
height="79.300003"
|
||||
rx="8.3900003"
|
||||
id="rect2" />
|
||||
<rect
|
||||
class="cls-3" fill="#202121"
|
||||
x="244.67999"
|
||||
y="289.66"
|
||||
width="125.32"
|
||||
height="67.370003"
|
||||
rx="2"
|
||||
id="rect3" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="M 151.55,352.34 H 194 a 3.83,3.83 0 0 1 3.83,3.83 v 1.39 h -50.11 v -1.39 a 3.83,3.83 0 0 1 3.83,-3.83 z"
|
||||
id="path3" />
|
||||
<path
|
||||
class="cls-1" fill="#474746"
|
||||
d="m 240,272 a 3,3 0 0 1 3,-2.69 h 24.48 a 3,3 0 0 1 3,2.61 L 240.25,272"
|
||||
id="path4" />
|
||||
<path
|
||||
class="cls-4" fill="#dbdcdd"
|
||||
d="m 353.11,172.61 a 54.23,54.23 0 0 0 -32.38,-10.67 h -49 a 54.49,54.49 0 0 0 -54.49,54.48 V 282 h -5.21 v -70 a 54.49,54.49 0 0 1 54.49,-54.48 h 49 a 54.24,54.24 0 0 1 37.59,15.09 z"
|
||||
id="path5" />
|
||||
<path
|
||||
class="cls-5" fill="#cbcbcc"
|
||||
d="m 370,212 v 51.84 h -21.49 l -9.24,-46.17 V 209 A 19.5,19.5 0 0 0 319.79,189.52 H 260.87 A 19.48,19.48 0 0 0 241.39,209 v 60.74 c -1.23,0.56 -0.86,2.13 -0.86,2.13 l -6.91,1.75 c -5.66,1.43 -7.63,4.78 -9.44,7 -1.81,2.22 -2,1.41 -2,1.41 h -4.89 v -65.61 a 54.49,54.49 0 0 1 54.49,-54.48 h 49 a 54.23,54.23 0 0 1 32.38,10.67 v 0 A 54.3,54.3 0 0 1 370,212 Z"
|
||||
id="path6" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 396.42,323.74 h 1.21 a 1.46,1.46 0 0 1 1.46,1.46 v 2.17 a 1.46,1.46 0 0 1 -1.46,1.46 h -1.21 z"
|
||||
id="path7" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="M 329.33,586.79 H 198 A 24.51,24.51 0 0 1 173.52,562.31 V 429 A 24.5,24.5 0 0 1 198,404.51 H 329.33 A 24.49,24.49 0 0 1 353.8,429 v 133.31 a 24.5,24.5 0 0 1 -24.47,24.48 z m -128.81,-27 H 326.8 V 431.51 H 200.52 Z"
|
||||
id="path8" />
|
||||
<rect
|
||||
class="cls-7" fill="#e5e2e5"
|
||||
x="187.02"
|
||||
y="418.01001"
|
||||
width="153.28"
|
||||
height="155.27"
|
||||
rx="10.97"
|
||||
id="rect8" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 166.62,386.56 a 12.85,12.85 0 1 0 12.86,12.85 12.85,12.85 0 0 0 -12.86,-12.85 z m 0,21 a 8.15,8.15 0 1 1 8.15,-8.15 8.14,8.14 0 0 1 -8.15,8.15 z"
|
||||
id="path9" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="m 174.77,399.41 a 8.15,8.15 0 1 1 -8.15,-8.15 8.15,8.15 0 0 1 8.15,8.15 z"
|
||||
id="path10" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 359.9,386.56 a 12.85,12.85 0 1 0 12.86,12.85 12.85,12.85 0 0 0 -12.86,-12.85 z m 0,21 a 8.15,8.15 0 1 1 8.15,-8.15 8.14,8.14 0 0 1 -8.15,8.15 z"
|
||||
id="path11" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="m 368.05,399.41 a 8.15,8.15 0 1 1 -8.15,-8.15 8.15,8.15 0 0 1 8.15,8.15 z"
|
||||
id="path12" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 359.9,579.56 a 12.86,12.86 0 1 0 12.86,12.85 12.85,12.85 0 0 0 -12.86,-12.85 z m 0,21 a 8.15,8.15 0 1 1 8.15,-8.15 8.14,8.14 0 0 1 -8.15,8.15 z"
|
||||
id="path13" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="m 368.05,592.41 a 8.15,8.15 0 1 1 -8.15,-8.14 8.15,8.15 0 0 1 8.15,8.14 z"
|
||||
id="path14" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 166.62,579.56 a 12.86,12.86 0 1 0 12.86,12.85 12.86,12.86 0 0 0 -12.86,-12.85 z m 0,21 a 8.15,8.15 0 1 1 8.15,-8.15 8.14,8.14 0 0 1 -8.15,8.15 z"
|
||||
id="path15" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="m 174.77,592.41 a 8.15,8.15 0 1 1 -8.15,-8.14 8.15,8.15 0 0 1 8.15,8.14 z"
|
||||
id="path16" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 253.12,315.37 h 5.46 v 15.27 h 8.51 v 5 h -14 z"
|
||||
id="path17" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 269.79,315.37 h 5.48 v 20.25 h -5.48 z"
|
||||
id="path18" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 279.18,315.37 h 5.46 v 15.27 h 8.52 v 5 h -14 z"
|
||||
id="path19" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 292,315.37 h 6 l 3.56,6.84 3.56,-6.84 h 6 l -6.87,11.77 v 8.48 h -5.47 v -8.48 z"
|
||||
id="path20" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 320.33,328.29 v -4.21 h 8.44 v 8.63 a 16.91,16.91 0 0 1 -4.28,2.58 13,13 0 0 1 -4.42,0.68 9.6,9.6 0 0 1 -5.13,-1.23 8,8 0 0 1 -3.07,-3.66 13.45,13.45 0 0 1 -1.09,-5.58 13.15,13.15 0 0 1 1.19,-5.77 8.3,8.3 0 0 1 3.5,-3.72 10,10 0 0 1 4.83,-1 11.86,11.86 0 0 1 4.38,0.61 5.69,5.69 0 0 1 2.41,1.88 8.8,8.8 0 0 1 1.44,3.24 l -5.27,1.08 a 3.24,3.24 0 0 0 -1.1,-1.76 3.15,3.15 0 0 0 -2,-0.6 3.36,3.36 0 0 0 -2.86,1.43 7.54,7.54 0 0 0 -1.07,4.52 c 0,2.19 0.36,3.76 1.08,4.7 a 3.57,3.57 0 0 0 3,1.41 5.11,5.11 0 0 0 1.75,-0.31 9.16,9.16 0 0 0 1.91,-1 v -1.91 z"
|
||||
id="path21" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 331.32,325.51 c 0,-3.3 0.81,-5.88 2.41,-7.72 a 8.46,8.46 0 0 1 6.72,-2.76 8.55,8.55 0 0 1 6.79,2.71 q 2.38,2.72 2.39,7.61 a 14.13,14.13 0 0 1 -1,5.82 8.19,8.19 0 0 1 -3,3.54 8.93,8.93 0 0 1 -4.91,1.26 10,10 0 0 1 -4.94,-1.09 8,8 0 0 1 -3.18,-3.45 12.89,12.89 0 0 1 -1.28,-5.92 z m 5.46,0 a 7.49,7.49 0 0 0 1,4.41 3.44,3.44 0 0 0 5.43,0 q 1,-1.31 1,-4.71 a 6.89,6.89 0 0 0 -1,-4.17 3.25,3.25 0 0 0 -2.73,-1.32 3.13,3.13 0 0 0 -2.65,1.34 7.6,7.6 0 0 0 -1.05,4.48 z"
|
||||
id="path22" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 356,322.64 v -4.46 h 1.89 a 3.22,3.22 0 0 1 1,0.12 1,1 0 0 1 0.52,0.43 1.3,1.3 0 0 1 0.2,0.7 1.18,1.18 0 0 1 -0.3,0.83 1.44,1.44 0 0 1 -0.88,0.42 2,2 0 0 1 0.48,0.37 5,5 0 0 1 0.51,0.72 l 0.55,0.87 h -1.08 l -0.65,-1 a 8,8 0 0 0 -0.47,-0.65 0.82,0.82 0 0 0 -0.28,-0.19 1.54,1.54 0 0 0 -0.45,-0.05 h -0.18 v 1.86 z m 0.9,-2.57 h 0.66 a 3.91,3.91 0 0 0 0.81,-0.05 0.51,0.51 0 0 0 0.26,-0.19 0.59,0.59 0 0 0 0.09,-0.34 0.52,0.52 0 0 0 -0.12,-0.36 0.62,0.62 0 0 0 -0.34,-0.18 h -1.36 z"
|
||||
id="path23" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 357.66,326.28 a 5.71,5.71 0 1 1 5.71,-5.71 5.71,5.71 0 0 1 -5.71,5.71 z m 0,-10.12 a 4.42,4.42 0 1 0 4.41,4.41 4.41,4.41 0 0 0 -4.41,-4.41 z"
|
||||
id="path24" />
|
||||
<path
|
||||
class="cls-9" fill="#1d1d1b"
|
||||
d="m 135.34,431.15 h 25.89 v 5.73 h -21.49 v 14.25 h -4.4 z"
|
||||
id="path25" />
|
||||
<path
|
||||
class="cls-9" fill="#1d1d1b"
|
||||
d="m 135.34,455 h 26.1 v 5.73 h -26.1 z"
|
||||
id="path26" />
|
||||
<path
|
||||
class="cls-9" fill="#1d1d1b"
|
||||
d="m 135.34,466.39 h 25.89 v 5.73 h -21.49 v 14.25 h -4.4 z"
|
||||
id="path27" />
|
||||
<path
|
||||
class="cls-9" fill="#1d1d1b"
|
||||
d="m 135.34,494.27 h 11 l 15.11,-10.4 v 6.72 l -10.32,6.68 10.32,6.55 v 6.6 L 146.29,500 h -10.95 z"
|
||||
id="path28" />
|
||||
<path
|
||||
class="cls-9" fill="#1d1d1b"
|
||||
d="m 144.94,526.48 h 4.4 v 12.35 h -10.4 a 15.78,15.78 0 0 1 -2.82,-5.22 20.31,20.31 0 0 1 -1.22,-6.92 16.61,16.61 0 0 1 1.72,-7.77 11.34,11.34 0 0 1 4.91,-5 14.83,14.83 0 0 1 6.95,-1.67 14,14 0 0 1 7.25,1.86 12.12,12.12 0 0 1 4.86,5.44 15.74,15.74 0 0 1 1.3,6.8 q 0,5.28 -2,8.26 a 9.16,9.16 0 0 1 -5.64,3.82 l -1,-5.69 a 5.51,5.51 0 0 0 3,-2.26 7.25,7.25 0 0 0 1.11,-4.13 8.14,8.14 0 0 0 -2.19,-6 q -2.19,-2.21 -6.49,-2.21 c -3.1,0 -5.43,0.75 -7,2.24 a 7.82,7.82 0 0 0 -2.33,5.89 10.72,10.72 0 0 0 0.65,3.61 12.93,12.93 0 0 0 1.58,3.11 h 3.31 z"
|
||||
id="path29" />
|
||||
<path
|
||||
class="cls-9" fill="#1d1d1b"
|
||||
d="m 148.23,543 a 15.54,15.54 0 0 1 6.7,1.3 12.53,12.53 0 0 1 3.58,2.64 11.41,11.41 0 0 1 2.35,3.67 17,17 0 0 1 1,6.12 q 0,6.27 -3.58,10 -3.58,3.73 -9.95,3.77 -6.33,0 -9.89,-3.74 -3.56,-3.74 -3.57,-10 0,-6.33 3.55,-10.07 3.55,-3.74 9.81,-3.69 z m 0.18,5.91 c -2.95,0 -5.2,0.74 -6.72,2.23 a 8.12,8.12 0 0 0 0,11.28 q 2.28,2.19 6.81,2.19 4.53,0 6.7,-2.14 a 8.43,8.43 0 0 0 0,-11.39 q -2.28,-2.22 -6.79,-2.22 z"
|
||||
id="path30" />
|
||||
<path
|
||||
class="cls-1" fill="#474746"
|
||||
d="m 384.27,274.39 v -3.8 a 6.7,6.7 0 0 0 -6.71,-6.71 h -73.09 a 6.7,6.7 0 0 0 -6.71,6.71 v 1.31 c 0,0 74.61,0 76.35,0 0.32,0 6.89,-0.2 10.16,2.49 z"
|
||||
id="path67" />
|
||||
<path
|
||||
class="cls-15" fill="#d1c854"
|
||||
d="m 223.32,282 a 21.84,21.84 0 0 0 -3.5,11.91 v 41.6 a 21.88,21.88 0 0 1 -3,11 L 204.7,339.3 a 8,8 0 0 1 -3.9,-6.86 V 290 a 8,8 0 0 1 8,-8 z"
|
||||
id="path68" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="m 374.11,275.9 a 18,18 0 0 1 18,18 v 311.39 a 18.05,18.05 0 0 1 -18,18 H 128.65 a 18.05,18.05 0 0 1 -18,-18 v -225.7 a 18,18 0 0 1 18,-18 h 69.14 a 26.05,26.05 0 0 0 26,-26 v -41.6 a 18,18 0 0 1 18,-18 h 132.32 m 0,-4 H 241.85 a 22,22 0 0 0 -22,22 v 41.6 a 22,22 0 0 1 -22,22 h -69.2 a 22,22 0 0 0 -22,22 v 225.7 a 22,22 0 0 0 22,22 h 245.46 a 22,22 0 0 0 22,-22 V 293.93 a 22,22 0 0 0 -22,-22 z"
|
||||
id="path69" />
|
||||
</g>
|
||||
<g
|
||||
id="Layer_7"
|
||||
data-name="Layer 7"
|
||||
transform="translate(-106,-157.045)">
|
||||
<path
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
d="m 396.14,293.93 v 311.35 a 22,22 0 0 1 -22,22 H 128.65 a 22,22 0 0 1 -22,-22 V 379.59 a 22,22 0 0 1 22,-22 h 69.14 a 22,22 0 0 0 22,-22 v -41.66 a 22,22 0 0 1 22,-22 h 132.32 a 22,22 0 0 1 22.03,22 z"
|
||||
id="path70" />
|
||||
<rect
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
x="118.67"
|
||||
y="378.97"
|
||||
width="268.88"
|
||||
height="233.91"
|
||||
rx="28.639999"
|
||||
id="rect70" />
|
||||
<rect
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
x="237.61"
|
||||
y="284.09"
|
||||
width="139.52"
|
||||
height="79.300003"
|
||||
rx="8.3900003"
|
||||
id="rect71" />
|
||||
<rect
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
x="244.67999"
|
||||
y="289.66"
|
||||
width="125.32"
|
||||
height="67.370003"
|
||||
rx="2"
|
||||
id="rect72" />
|
||||
<path
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
d="M 151.55,352.34 H 194 a 3.83,3.83 0 0 1 3.83,3.83 v 1.39 h -50.11 v -1.39 a 3.83,3.83 0 0 1 3.83,-3.83 z"
|
||||
id="path72" />
|
||||
<path
|
||||
class="cls-16" stroke-width="1.01px"
|
||||
d="m 240,272 a 3,3 0 0 1 3,-2.69 h 24.48 a 3,3 0 0 1 3,2.61"
|
||||
id="path73" />
|
||||
<rect
|
||||
class="cls-10" fill="#d1b066"
|
||||
x="322.39001"
|
||||
y="253.42999"
|
||||
width="37.77"
|
||||
height="10.45"
|
||||
id="rect73" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,250.81 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.63 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path74" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,248.28 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.63 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path75" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,245.75 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path76" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,243.22 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path77" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,240.69 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path78" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,238.17 -1,0.64 a 0.67,0.67 0 0 0 0,1.15 l 1,0.64 26.39,0.09 1,-0.63 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path79" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,235.64 -1,0.64 a 0.67,0.67 0 0 0 0,1.15 l 1,0.64 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path80" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,233.11 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.63 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path81" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,230.58 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path82" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,228.05 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path83" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,225.52 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path84" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,223 -1,0.64 a 0.67,0.67 0 0 0 0,1.15 l 1,0.64 26.39,0.09 1,-0.63 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path85" />
|
||||
<rect
|
||||
class="cls-10" fill="#d1b066"
|
||||
x="328.20001"
|
||||
y="217.71001"
|
||||
width="26.27"
|
||||
height="4.9099998"
|
||||
id="rect85" />
|
||||
<rect
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
x="322.63"
|
||||
y="253.42999"
|
||||
width="18.77"
|
||||
height="10.45"
|
||||
id="rect86" />
|
||||
<rect
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
x="341.39999"
|
||||
y="253.42999"
|
||||
width="18.77"
|
||||
height="10.45"
|
||||
id="rect87" />
|
||||
<rect
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
x="328.20001"
|
||||
y="217.71001"
|
||||
width="26.27"
|
||||
height="5.29"
|
||||
id="rect88" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,250.81 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.63 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path88" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,248.28 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.63 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path89" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,245.75 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path90" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,243.22 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path91" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,240.69 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path92" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,238.17 -1,0.64 a 0.67,0.67 0 0 0 0,1.15 l 1,0.64 26.39,0.09 1,-0.63 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path93" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,235.64 -1,0.64 a 0.67,0.67 0 0 0 0,1.15 l 1,0.64 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path94" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,233.11 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.63 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path95" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,230.58 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path96" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,228.05 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path97" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,225.52 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path98" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,223 -1,0.64 a 0.67,0.67 0 0 0 0,1.15 l 1,0.64 26.39,0.09 1,-0.63 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path99" />
|
||||
<path
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
d="m 384.27,274.39 v 0 -3.79 a 6.7,6.7 0 0 0 -6.71,-6.71 h -73.09 a 6.7,6.7 0 0 0 -6.71,6.71 v 1.31"
|
||||
id="path100" />
|
||||
<path
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
d="m 223.32,282 h -14.53 a 8,8 0 0 0 -8,8 v 42.43 a 8,8 0 0 0 3.9,6.86 l 12.16,7.25"
|
||||
id="path101" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 212.06,282 v -70 a 54.49,54.49 0 0 1 54.49,-54.48 h 49 A 54.48,54.48 0 0 1 370,212 v 51.84"
|
||||
id="path102" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="M 241.39,269.77 V 209 a 19.48,19.48 0 0 1 19.48,-19.48 h 58.92 A 19.48,19.48 0 0 1 339.27,209 v 8.68"
|
||||
id="path103" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 396.42,323.74 h 1.21 a 1.46,1.46 0 0 1 1.46,1.46 v 2.17 a 1.46,1.46 0 0 1 -1.46,1.46 h -1.21 z"
|
||||
id="path104" />
|
||||
<rect
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
x="187.02"
|
||||
y="418.01001"
|
||||
width="153.28"
|
||||
height="155.27"
|
||||
rx="10.97"
|
||||
id="rect104" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="166.62"
|
||||
cy="399.41"
|
||||
r="8.1499996"
|
||||
id="circle140" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="166.62"
|
||||
cy="399.41"
|
||||
r="12.85"
|
||||
id="circle141" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="359.89999"
|
||||
cy="399.41"
|
||||
r="8.1499996"
|
||||
id="circle142" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="359.89999"
|
||||
cy="399.41"
|
||||
r="12.85"
|
||||
id="circle143" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="359.89999"
|
||||
cy="592.40997"
|
||||
r="8.1499996"
|
||||
id="circle144" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="359.89999"
|
||||
cy="592.40997"
|
||||
r="12.85"
|
||||
id="circle145" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="166.62"
|
||||
cy="592.40997"
|
||||
r="8.1499996"
|
||||
id="circle146" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="166.62"
|
||||
cy="592.40997"
|
||||
r="12.85"
|
||||
id="circle147" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 253.12,315.37 h 5.46 v 15.27 h 8.51 v 5 h -14 z"
|
||||
id="path147" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 269.79,315.37 h 5.48 v 20.25 h -5.48 z"
|
||||
id="path148" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 279.18,315.37 h 5.46 v 15.27 h 8.52 v 5 h -14 z"
|
||||
id="path149" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 292,315.37 h 6 l 3.56,6.84 3.56,-6.84 h 6 l -6.87,11.77 v 8.48 h -5.47 v -8.48 z"
|
||||
id="path150" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 320.33,328.29 v -4.21 h 8.44 v 8.63 a 16.91,16.91 0 0 1 -4.28,2.58 13,13 0 0 1 -4.42,0.68 9.6,9.6 0 0 1 -5.13,-1.23 8,8 0 0 1 -3.07,-3.66 13.45,13.45 0 0 1 -1.09,-5.58 13.15,13.15 0 0 1 1.19,-5.77 8.3,8.3 0 0 1 3.5,-3.72 10,10 0 0 1 4.83,-1 11.86,11.86 0 0 1 4.38,0.61 5.69,5.69 0 0 1 2.41,1.88 8.8,8.8 0 0 1 1.44,3.24 l -5.27,1.08 a 3.24,3.24 0 0 0 -1.1,-1.76 3.15,3.15 0 0 0 -2,-0.6 3.36,3.36 0 0 0 -2.86,1.43 7.54,7.54 0 0 0 -1.07,4.52 c 0,2.19 0.36,3.76 1.08,4.7 a 3.57,3.57 0 0 0 3,1.41 5.11,5.11 0 0 0 1.75,-0.31 9.16,9.16 0 0 0 1.91,-1 v -1.91 z"
|
||||
id="path151" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 331.32,325.51 c 0,-3.3 0.81,-5.88 2.41,-7.72 a 8.46,8.46 0 0 1 6.72,-2.76 8.55,8.55 0 0 1 6.79,2.71 q 2.38,2.72 2.39,7.61 a 14.13,14.13 0 0 1 -1,5.82 8.19,8.19 0 0 1 -3,3.54 8.93,8.93 0 0 1 -4.91,1.26 10,10 0 0 1 -4.94,-1.09 8,8 0 0 1 -3.18,-3.45 12.89,12.89 0 0 1 -1.28,-5.92 z m 5.46,0 a 7.49,7.49 0 0 0 1,4.41 3.44,3.44 0 0 0 5.43,0 q 1,-1.31 1,-4.71 a 6.89,6.89 0 0 0 -1,-4.17 3.25,3.25 0 0 0 -2.73,-1.32 3.13,3.13 0 0 0 -2.65,1.34 7.6,7.6 0 0 0 -1.05,4.48 z"
|
||||
id="path152" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 356,322.64 v -4.46 h 1.89 a 3.22,3.22 0 0 1 1,0.12 1,1 0 0 1 0.52,0.43 1.3,1.3 0 0 1 0.2,0.7 1.18,1.18 0 0 1 -0.3,0.83 1.44,1.44 0 0 1 -0.88,0.42 2,2 0 0 1 0.48,0.37 5,5 0 0 1 0.51,0.72 l 0.55,0.87 h -1.08 l -0.65,-1 a 8,8 0 0 0 -0.47,-0.65 0.82,0.82 0 0 0 -0.28,-0.19 1.54,1.54 0 0 0 -0.45,-0.05 h -0.18 v 1.86 z m 0.9,-2.57 h 0.66 a 3.91,3.91 0 0 0 0.81,-0.05 0.51,0.51 0 0 0 0.26,-0.19 0.59,0.59 0 0 0 0.09,-0.34 0.52,0.52 0 0 0 -0.12,-0.36 0.62,0.62 0 0 0 -0.34,-0.18 h -1.36 z"
|
||||
id="path153" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 357.66,326.28 a 5.71,5.71 0 1 1 5.71,-5.71 5.71,5.71 0 0 1 -5.71,5.71 z m 0,-10.12 a 4.42,4.42 0 1 0 4.41,4.41 4.41,4.41 0 0 0 -4.41,-4.41 z"
|
||||
id="path154" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 135.34,431.15 h 25.89 v 5.73 h -21.49 v 14.25 h -4.4 z"
|
||||
id="path155" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 135.34,455 h 26.1 v 5.73 h -26.1 z"
|
||||
id="path156" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 135.34,466.39 h 25.89 v 5.73 h -21.49 v 14.25 h -4.4 z"
|
||||
id="path157" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 135.34,494.27 h 11 l 15.11,-10.4 v 6.72 l -10.32,6.68 10.32,6.55 v 6.6 L 146.29,500 h -10.95 z"
|
||||
id="path158" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 144.94,526.48 h 4.4 v 12.35 h -10.4 a 15.78,15.78 0 0 1 -2.82,-5.22 20.31,20.31 0 0 1 -1.22,-6.92 16.61,16.61 0 0 1 1.72,-7.77 11.34,11.34 0 0 1 4.91,-5 14.83,14.83 0 0 1 6.95,-1.67 14,14 0 0 1 7.25,1.86 12.12,12.12 0 0 1 4.86,5.44 15.74,15.74 0 0 1 1.3,6.8 q 0,5.28 -2,8.26 a 9.16,9.16 0 0 1 -5.64,3.82 l -1,-5.69 a 5.51,5.51 0 0 0 3,-2.26 7.25,7.25 0 0 0 1.11,-4.13 8.14,8.14 0 0 0 -2.19,-6 q -2.19,-2.21 -6.49,-2.21 c -3.1,0 -5.43,0.75 -7,2.24 a 7.82,7.82 0 0 0 -2.33,5.89 10.72,10.72 0 0 0 0.65,3.61 12.93,12.93 0 0 0 1.58,3.11 h 3.31 z"
|
||||
id="path159" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 148.23,543 a 15.54,15.54 0 0 1 6.7,1.3 12.53,12.53 0 0 1 3.58,2.64 11.41,11.41 0 0 1 2.35,3.67 17,17 0 0 1 1,6.12 q 0,6.27 -3.58,10 -3.58,3.73 -9.95,3.77 -6.33,0 -9.89,-3.74 -3.56,-3.74 -3.57,-10 0,-6.33 3.55,-10.07 3.55,-3.74 9.81,-3.69 z m 0.18,5.91 c -2.95,0 -5.2,0.74 -6.72,2.23 a 8.12,8.12 0 0 0 0,11.28 q 2.28,2.19 6.81,2.19 4.53,0 6.7,-2.14 a 8.43,8.43 0 0 0 0,-11.39 q -2.28,-2.22 -6.79,-2.22 z"
|
||||
id="path160" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 123 KiB |
@@ -1,109 +0,0 @@
|
||||
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1" id="svg105" sodipodi:docname="thinknode_m1.svg" inkscape:version="1.4 (e7c3feb1, 2024-10-09)" viewBox="397.31 77.24 361 863.17">
|
||||
<sodipodi:namedview id="namedview105" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="4.066786" inkscape:cx="564.94244" inkscape:cy="741.49463" inkscape:window-width="1472" inkscape:window-height="890" inkscape:window-x="0" inkscape:window-y="38" inkscape:window-maximized="1" inkscape:current-layer="Layer_3"/>
|
||||
<defs id="defs1">
|
||||
<style id="style1">.cls-1{fill:#353535;}.cls-2{fill:#262626;}.cls-3{fill:#cccccb;}.cls-4{fill:#2b2b2b;}.cls-5{fill:#f05043;}.cls-6{fill:#3d3d3d;}.cls-7{fill:#231f20;}.cls-8{fill:none;stroke:#000;stroke-miterlimit:10;}</style>
|
||||
</defs>
|
||||
<g id="Layer_3" data-name="Layer 3">
|
||||
<path class="cls-1" fill="#353535" d="M720.82,449.91h11.45a19.68,19.68,0,0,1,19.67,19.68V905.12a28.48,28.48,0,0,1-28.47,28.48H425.72A27.77,27.77,0,0,1,397.81,906V470.82a21,21,0,0,1,21.13-20.91h23.74" id="path1"/>
|
||||
<rect class="cls-2" fill="#262626" x="447.12" y="523.83" width="266.09" height="266.09" rx="22.7" id="rect1"/>
|
||||
<rect class="cls-1" fill="#353535" x="465.51" y="542.22" width="229.3" height="229.3" rx="12.91" id="rect2"/>
|
||||
<rect class="cls-3" fill="#cccccb" x="476.07" y="552.78" width="208.17" height="208.17" rx="7.83" id="rect3"/>
|
||||
<path class="cls-1" fill="#353535" d="M507.38,77.74H472.16a7,7,0,0,0-7,7V359.93L452.2,396.26v39.91H561V396.26l-13.3-36V84.15a6.41,6.41,0,0,0-6.41-6.41Z" id="path3"/>
|
||||
<rect class="cls-2" fill="#262626" x="454.25" y="436.17" width="104.38" height="3.65" id="rect4"/>
|
||||
<polygon class="cls-1" fill="#353535" points="442.68 449.91 448.16 440.69 562.98 440.69 570.51 449.91 442.68 449.91" id="polygon4"/>
|
||||
<rect class="cls-1" fill="#353535" x="604.37" y="355.96" width="105.26" height="60.65" rx="4.8" id="rect5"/>
|
||||
<path class="cls-2" fill="#262626" d="M611.2,356v-5.48a3.13,3.13,0,0,1,3.13-3.13h86.35a3.13,3.13,0,0,1,3.13,3.13V356Z" id="path5"/>
|
||||
<rect class="cls-2" fill="#262626" x="611.07" y="416.61" width="92.74" height="23.22" id="rect6"/>
|
||||
<polygon class="cls-1" fill="#353535" points="592.99 449.91 598.47 440.69 713.42 440.69 720.82 449.91 592.99 449.91" id="polygon6"/>
|
||||
<rect class="cls-2" fill="#262626" x="751.94" y="555.13" width="5.87" height="47.48" id="rect7"/>
|
||||
<path class="cls-2" fill="#262626" d="M751.94,683.87h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V683.87A0,0,0,0,1,751.94,683.87Z" id="path7"/>
|
||||
<path class="cls-2" fill="#262626" d="M751.94,781.43h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V781.43A0,0,0,0,1,751.94,781.43Z" id="path8"/>
|
||||
<path class="cls-4" fill="#2b2b2b" d="M425.72,933.6l17.46-41.05a15.2,15.2,0,0,1,14-9.25H702.88A15.19,15.19,0,0,1,717,892.9l15.52,39.22" id="path9"/>
|
||||
<rect class="cls-2" fill="#262626" x="505.03" y="841.57" width="147.52" height="24.65" rx="12.33" id="rect9"/>
|
||||
<circle class="cls-5" fill="#f05043" cx="518.72" cy="853.89" r="5.48" id="circle9"/>
|
||||
<circle class="cls-1" fill="#353535" cx="640.14" cy="853.89" r="5.48" id="circle10"/>
|
||||
<circle class="cls-1" fill="#353535" cx="541.83" cy="853.89" r="5.48" id="circle11"/>
|
||||
<circle class="cls-1" fill="#353535" cx="567.67" cy="853.89" r="5.48" id="circle12"/>
|
||||
<circle class="cls-1" fill="#353535" cx="593.51" cy="853.89" r="5.48" id="circle13"/>
|
||||
<circle class="cls-1" fill="#353535" cx="616.82" cy="853.89" r="5.48" id="circle14"/>
|
||||
<path class="cls-4" fill="#2b2b2b" d="M428.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path14"/>
|
||||
<path class="cls-4" fill="#2b2b2b" d="M713.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path15"/>
|
||||
<path class="cls-6" fill="#3d3d3d" d="M494.46,449.91v5.59a12.22,12.22,0,0,0,1.05,4.95l8.6,19.42a9.43,9.43,0,0,0,8.62,5.61h3.61a9.43,9.43,0,0,0,9.43-9.43V449.91" id="path16"/>
|
||||
<path class="cls-6" fill="#3d3d3d" d="M672.56,449.91v5.59a12.22,12.22,0,0,1-1,4.95l-8.6,19.42a9.43,9.43,0,0,1-8.62,5.61h-3.61a9.43,9.43,0,0,1-9.43-9.43V449.91" id="path17"/>
|
||||
<path class="cls-6" fill="#3d3d3d" d="M532.42,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,532.42,449.91Z" id="path18"/>
|
||||
<path class="cls-6" fill="#3d3d3d" d="M559.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,559.81,449.91Z" id="path19"/>
|
||||
<path class="cls-6" fill="#3d3d3d" d="M587.2,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,587.2,449.91Z" id="path20"/>
|
||||
<path class="cls-6" fill="#3d3d3d" d="M613.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,613.81,449.91Z" id="path21"/>
|
||||
<path class="cls-1" fill="#353535" d="M477,924.32h-3V903.09h-7.46v-2.65h17.9v2.65H477Z" id="path51"/>
|
||||
<path class="cls-1" fill="#353535" d="M490.59,906.36c0,.43,0,.86,0,1.31s-.07.85-.12,1.2h.2a5.17,5.17,0,0,1,1.44-1.54,7.08,7.08,0,0,1,1.94-.92,7.81,7.81,0,0,1,2.21-.31,8.61,8.61,0,0,1,3.63.68,4.62,4.62,0,0,1,2.19,2.13,8.22,8.22,0,0,1,.73,3.74v11.67h-2.9V912.85a4.72,4.72,0,0,0-1-3.24,3.92,3.92,0,0,0-3.05-1.07,5.65,5.65,0,0,0-3.14.75,4.07,4.07,0,0,0-1.62,2.21,11.17,11.17,0,0,0-.49,3.56v9.26h-2.94V898.91h2.94Z" id="path52"/>
|
||||
<path class="cls-1" fill="#353535" d="M509.82,899.67a1.73,1.73,0,0,1,1.19.46,2.17,2.17,0,0,1,0,2.82,1.74,1.74,0,0,1-1.19.47,1.77,1.77,0,0,1-1.24-.47,2.24,2.24,0,0,1,0-2.82A1.76,1.76,0,0,1,509.82,899.67Zm1.44,6.73v17.92h-2.94V906.4Z" id="path53"/>
|
||||
<path class="cls-1" fill="#353535" d="M525.58,906.06a6.8,6.8,0,0,1,4.85,1.56c1.09,1,1.63,2.71,1.63,5v11.67h-2.91V912.85a4.67,4.67,0,0,0-1-3.24,3.88,3.88,0,0,0-3-1.07c-2,0-3.36.56-4.11,1.67a8.54,8.54,0,0,0-1.14,4.82v9.29H517V906.4h2.37l.44,2.44h.16a5.68,5.68,0,0,1,1.49-1.56,6.41,6.41,0,0,1,2-.92A8.13,8.13,0,0,1,525.58,906.06Z" id="path54"/>
|
||||
<path class="cls-1" fill="#353535" d="M540.53,912.18c0,.36,0,.83,0,1.41s-.07,1.08-.09,1.5h.14l.6-.77.82-1c.28-.34.52-.63.72-.85l5.72-6.05h3.44l-7.26,7.66,7.76,10.26h-3.54L542.57,916l-2,1.78v6.58h-2.91V898.91h2.91Z" id="path55"/>
|
||||
<path class="cls-1" fill="#353535" d="M574.81,924.32H571.3l-12.78-19.83h-.13c0,.51.08,1.12.11,1.82s.07,1.45.1,2.24,0,1.61,0,2.43v13.34h-2.77V900.44h3.48l12.74,19.77h.13c0-.36-.05-.89-.08-1.6s-.07-1.5-.1-2.35,0-1.62,0-2.34V900.44h2.81Z" id="path56"/>
|
||||
<path class="cls-1" fill="#353535" d="M596.48,915.33a12.32,12.32,0,0,1-.58,4,8.32,8.32,0,0,1-1.67,2.93,7,7,0,0,1-2.65,1.82,9.31,9.31,0,0,1-3.46.62,8.63,8.63,0,0,1-3.28-.62,7.29,7.29,0,0,1-2.61-1.82,8.57,8.57,0,0,1-1.72-2.93,11.76,11.76,0,0,1-.62-4,11.43,11.43,0,0,1,1-5,7.12,7.12,0,0,1,2.87-3.14,8.76,8.76,0,0,1,4.45-1.09,8.4,8.4,0,0,1,4.3,1.09,7.45,7.45,0,0,1,2.91,3.14A11,11,0,0,1,596.48,915.33Zm-13.54,0a10.93,10.93,0,0,0,.55,3.66,4.82,4.82,0,0,0,1.72,2.39,5.69,5.69,0,0,0,5.95,0,4.78,4.78,0,0,0,1.73-2.39,10.93,10.93,0,0,0,.55-3.66,10.36,10.36,0,0,0-.57-3.65,4.84,4.84,0,0,0-1.72-2.32,5,5,0,0,0-3-.82,4.5,4.5,0,0,0-4,1.8A8.79,8.79,0,0,0,582.94,915.33Z" id="path57"/>
|
||||
<path class="cls-1" fill="#353535" d="M607.49,924.66a6.67,6.67,0,0,1-5.35-2.33c-1.34-1.54-2-3.86-2-6.94s.67-5.4,2-7a6.74,6.74,0,0,1,5.37-2.36,7.71,7.71,0,0,1,2.44.35,6.37,6.37,0,0,1,1.81,1,6.58,6.58,0,0,1,1.3,1.34h.2c0-.29-.06-.72-.11-1.29s-.09-1-.09-1.36v-7.15H616v25.41h-2.38l-.43-2.4h-.14a6.51,6.51,0,0,1-1.3,1.38,5.9,5.9,0,0,1-1.82,1A7.4,7.4,0,0,1,607.49,924.66Zm.46-2.44c1.9,0,3.23-.52,4-1.56a7.84,7.84,0,0,0,1.16-4.7v-.53a9.84,9.84,0,0,0-1.11-5.14c-.73-1.19-2.09-1.79-4.08-1.79a4,4,0,0,0-3.56,1.89,9.46,9.46,0,0,0-1.19,5.07,9,9,0,0,0,1.19,5A4,4,0,0,0,608,922.22Z" id="path58"/>
|
||||
<path class="cls-1" fill="#353535" d="M628.62,906.06a7.53,7.53,0,0,1,4,1,6.62,6.62,0,0,1,2.54,2.82,9.69,9.69,0,0,1,.89,4.27v1.77H623.74a6.75,6.75,0,0,0,1.56,4.63,5.42,5.42,0,0,0,4.16,1.59,12.58,12.58,0,0,0,3-.32,16.78,16.78,0,0,0,2.72-.92v2.58a13.84,13.84,0,0,1-2.71.89,16.15,16.15,0,0,1-3.17.28,9.46,9.46,0,0,1-4.5-1,7.15,7.15,0,0,1-3-3.09,10.61,10.61,0,0,1-1.09-5,12,12,0,0,1,1-5,7.33,7.33,0,0,1,6.94-4.38Zm0,2.41a4.26,4.26,0,0,0-3.33,1.36,6.34,6.34,0,0,0-1.45,3.76h9.13a7.05,7.05,0,0,0-.47-2.68,3.94,3.94,0,0,0-1.42-1.79A4.33,4.33,0,0,0,628.59,908.47Z" id="path59"/>
|
||||
<path class="cls-1" fill="#353535" d="M639.36,913h8.1v2.74h-8.1Z" id="path60"/>
|
||||
<path class="cls-1" fill="#353535" d="M662.87,924.32,655,903.39h-.13c0,.44.08,1,.12,1.7s.06,1.45.08,2.26,0,1.65,0,2.49v14.48h-2.77V900.44h4.45L664.15,920h.13l7.49-19.57h4.42v23.88h-3V909.64c0-.78,0-1.55,0-2.32s.06-1.5.1-2.18.08-1.25.1-1.72h-.13l-8,20.9Z" id="path61"/>
|
||||
<path class="cls-1" fill="#353535" d="M688.16,924.32V909.41c0-.63,0-1.3,0-2s0-1.42.07-2.1,0-1.27,0-1.76c-.35.38-.66.69-.92.92s-.6.54-1,.92l-2.41,2-1.57-2,6.26-4.89H691v23.88Z" id="path62"/>
|
||||
<path class="cls-1" fill="#353535" d="M502.55,894.19l-2.22-2.37a14.1,14.1,0,0,1,18.94-.33l-2.13,2.44a10.9,10.9,0,0,0-7.16-2.69A10.78,10.78,0,0,0,502.55,894.19Z" id="path63"/>
|
||||
<path class="cls-1" fill="#353535" d="M506.38,897.85l-2.4-2.18a8.08,8.08,0,0,1,11.61-.39l-2.24,2.33a4.85,4.85,0,0,0-7,.24Z" id="path64"/>
|
||||
</g>
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M472.55,77.74h68.09a7.43,7.43,0,0,1,7.43,7.43V360.26a0,0,0,0,1,0,0h-83a0,0,0,0,1,0,0V85.17A7.43,7.43,0,0,1,472.55,77.74Z" id="path65"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="465.12" y1="123.91" x2="548.07" y2="123.91" id="line65"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="465.12" y1="149.74" x2="548.07" y2="149.74" id="line66"/>
|
||||
<polyline class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" points="465.12 360.26 452.2 396.26 452.2 436.17 560.99 436.17 560.99 396.26 548.07 360.26" id="polyline66"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="452.2" y1="396.26" x2="560.98" y2="396.26" id="line67"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M449.69,440.17H562a3.26,3.26,0,0,1,2.56,1.55l5.93,8.19H442.68l4-7.49A3.65,3.65,0,0,1,449.69,440.17Z" id="path67"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M600,440.17H712.33a3.24,3.24,0,0,1,2.56,1.55l5.93,8.19H593l4-7.49A3.65,3.65,0,0,1,600,440.17Z" id="path68"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="454.45" y1="436.17" x2="454.45" y2="439.83" id="line68"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="558.64" y1="436.17" x2="558.64" y2="439.83" id="line69"/>
|
||||
<rect class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x="604.37" y="355.96" width="105.26" height="60.65" rx="4.87" id="rect69"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="611.07" y1="416.61" x2="611.07" y2="439.83" id="line70"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="703.81" y1="416.61" x2="703.81" y2="439.83" id="line71"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M614.2,347.35h86.48a3.13,3.13,0,0,1,3.13,3.13V356a0,0,0,0,1,0,0H611.07a0,0,0,0,1,0,0v-5.48A3.13,3.13,0,0,1,614.2,347.35Z" id="path71"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="570.51" y1="449.91" x2="592.99" y2="449.91" id="line72"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M720.82,449.91h11.45a19.68,19.68,0,0,1,19.67,19.68V905.12a28.48,28.48,0,0,1-28.47,28.48H425.72A27.77,27.77,0,0,1,397.81,906V470.82a21,21,0,0,1,21.13-20.91h23.74" id="path72"/>
|
||||
<rect class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x="447.12" y="523.83" width="266.09" height="266.09" rx="22.7" id="rect72"/>
|
||||
<rect class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x="465.51" y="542.22" width="229.3" height="229.3" rx="12.91" id="rect73"/>
|
||||
<rect class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x="476.07" y="552.78" width="208.17" height="208.17" rx="7.83" id="rect74"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M494.46,449.91v5.59a12.22,12.22,0,0,0,1.05,4.95l8.6,19.42a9.43,9.43,0,0,0,8.62,5.61h3.61a9.43,9.43,0,0,0,9.43-9.43V449.91" id="path74"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M672.56,449.91v5.59a12.22,12.22,0,0,1-1,4.95l-8.6,19.42a9.43,9.43,0,0,1-8.62,5.61h-3.61a9.43,9.43,0,0,1-9.43-9.43V449.91" id="path75"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M532.42,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,532.42,449.91Z" id="path76"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M559.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,559.81,449.91Z" id="path77"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M587.2,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,587.2,449.91Z" id="path78"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M613.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,613.81,449.91Z" id="path79"/>
|
||||
<rect class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x="751.94" y="555.13" width="5.87" height="47.48" id="rect79"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M751.94,683.87h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V683.87A0,0,0,0,1,751.94,683.87Z" id="path80"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M751.94,781.43h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V781.43A0,0,0,0,1,751.94,781.43Z" id="path81"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M425.72,933.6l17.46-41.05a15.2,15.2,0,0,1,14-9.25H702.88A15.19,15.19,0,0,1,717,892.9l15.52,39.22" id="path82"/>
|
||||
<rect class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x="505.03" y="841.57" width="147.52" height="24.65" rx="12.33" id="rect82"/>
|
||||
<circle class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" cx="518.72" cy="853.89" r="5.48" id="circle82"/>
|
||||
<circle class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" cx="640.14" cy="853.89" r="5.48" id="circle83"/>
|
||||
<circle class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" cx="541.83" cy="853.89" r="5.48" id="circle84"/>
|
||||
<circle class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" cx="567.67" cy="853.89" r="5.48" id="circle85"/>
|
||||
<circle class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" cx="593.51" cy="853.89" r="5.48" id="circle86"/>
|
||||
<circle class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" cx="616.82" cy="853.89" r="5.48" id="circle87"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="430.68" y1="572.74" x2="430.68" y2="602.61" id="line87"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="424.42" y1="595.43" x2="424.42" y2="578.11" id="line88"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="438.21" y1="595.43" x2="438.21" y2="578.11" id="line89"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="430.68" y1="644.74" x2="430.68" y2="674.61" id="line90"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="424.42" y1="667.43" x2="424.42" y2="650.11" id="line91"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="438.21" y1="667.43" x2="438.21" y2="650.11" id="line92"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="430.68" y1="716.74" x2="430.68" y2="746.61" id="line93"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="424.42" y1="739.43" x2="424.42" y2="722.11" id="line94"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="438.21" y1="739.43" x2="438.21" y2="722.11" id="line95"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="730.03" y1="572.74" x2="730.03" y2="602.61" id="line96"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="723.77" y1="595.43" x2="723.77" y2="578.11" id="line97"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="737.56" y1="595.43" x2="737.56" y2="578.11" id="line98"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="730.03" y1="644.74" x2="730.03" y2="674.61" id="line99"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="723.77" y1="667.43" x2="723.77" y2="650.11" id="line100"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="737.56" y1="667.43" x2="737.56" y2="650.11" id="line101"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="730.03" y1="716.74" x2="730.03" y2="746.61" id="line102"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="723.77" y1="739.43" x2="723.77" y2="722.11" id="line103"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="737.56" y1="739.43" x2="737.56" y2="722.11" id="line104"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M428.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path104"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M713.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path105"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
@@ -1,264 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 233.57587 210"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg22"
|
||||
sodipodi:docname="unknown-new.svg"
|
||||
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
||||
width="233.57587"
|
||||
height="210"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview22"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="2.6448247"
|
||||
inkscape:cx="116.45384"
|
||||
inkscape:cy="98.116143"
|
||||
inkscape:window-width="1728"
|
||||
inkscape:window-height="1056"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="33"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg22" />
|
||||
<!-- Antenna -->
|
||||
<!-- Radio body - main housing with gradient -->
|
||||
<defs
|
||||
id="defs6">
|
||||
<linearGradient
|
||||
id="bodyGrad"
|
||||
x1="29.580399"
|
||||
y1="34.651325"
|
||||
x2="207.06279"
|
||||
y2="212.13371"
|
||||
gradientTransform="matrix(0.84515425,0,0,1.183216,16.289544,-41)"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0%"
|
||||
style="stop-color:#22222a"
|
||||
id="stop2" />
|
||||
<stop
|
||||
offset="100%"
|
||||
style="stop-color:#16161b"
|
||||
id="stop3" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="screenGrad"
|
||||
x1="36.829224"
|
||||
y1="105.71089"
|
||||
x2="36.829224"
|
||||
y2="201.38222"
|
||||
gradientTransform="matrix(1.1811273,0,0,0.84664878,16.289544,-41)"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0%"
|
||||
style="stop-color:#0c1a12"
|
||||
id="stop4" />
|
||||
<stop
|
||||
offset="100%"
|
||||
style="stop-color:#061008"
|
||||
id="stop5" />
|
||||
</linearGradient>
|
||||
<filter
|
||||
id="glow"
|
||||
x="-0.8"
|
||||
y="-0.8"
|
||||
width="2.6"
|
||||
height="2.6">
|
||||
<feGaussianBlur
|
||||
stdDeviation="2"
|
||||
result="coloredBlur"
|
||||
id="feGaussianBlur5" />
|
||||
<feMerge
|
||||
id="feMerge6">
|
||||
<feMergeNode
|
||||
in="coloredBlur"
|
||||
id="feMergeNode5" />
|
||||
<feMergeNode
|
||||
in="SourceGraphic"
|
||||
id="feMergeNode6" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect
|
||||
x="46.289543"
|
||||
y="5"
|
||||
width="140"
|
||||
height="200"
|
||||
rx="14"
|
||||
fill="url(#bodyGrad)"
|
||||
stroke="#67ea94"
|
||||
stroke-width="10"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="rect6"
|
||||
style="fill:url(#bodyGrad)" />
|
||||
<!-- Inner bezel -->
|
||||
<rect
|
||||
x="54.289543"
|
||||
y="13"
|
||||
width="124"
|
||||
height="184"
|
||||
rx="10"
|
||||
fill="none"
|
||||
stroke="#2a2a32"
|
||||
stroke-width="1"
|
||||
id="rect7" />
|
||||
<!-- Top speaker grille -->
|
||||
<rect
|
||||
x="71.289543"
|
||||
y="21"
|
||||
width="90"
|
||||
height="20"
|
||||
rx="3"
|
||||
fill="#0a0a0f"
|
||||
id="rect8" />
|
||||
<g
|
||||
stroke="#333340"
|
||||
stroke-width="1"
|
||||
id="g10"
|
||||
transform="translate(16.289544,-41)">
|
||||
<line
|
||||
x1="62"
|
||||
y1="68"
|
||||
x2="138"
|
||||
y2="68"
|
||||
id="line8" />
|
||||
<line
|
||||
x1="62"
|
||||
y1="72"
|
||||
x2="138"
|
||||
y2="72"
|
||||
id="line9" />
|
||||
<line
|
||||
x1="62"
|
||||
y1="76"
|
||||
x2="138"
|
||||
y2="76"
|
||||
id="line10" />
|
||||
</g>
|
||||
<!-- Screen area with glow effect -->
|
||||
<rect
|
||||
x="60.289543"
|
||||
y="49"
|
||||
width="112"
|
||||
height="80"
|
||||
rx="6"
|
||||
fill="url(#screenGrad)"
|
||||
stroke="#67ea94"
|
||||
stroke-width="1"
|
||||
opacity="0.8"
|
||||
id="rect10"
|
||||
style="fill:url(#screenGrad)" />
|
||||
<!-- Meshtastic logo centered in screen - using actual logo paths -->
|
||||
<g
|
||||
transform="matrix(0.95,0,0,0.95,69.289544,64)"
|
||||
filter="url(#glow)"
|
||||
id="g14">
|
||||
<g
|
||||
transform="matrix(0.802386,0,0,0.460028,-421.748,-122.127)"
|
||||
id="g13">
|
||||
<g
|
||||
transform="matrix(0.579082,0,0,1.01004,460.975,-39.6867)"
|
||||
id="g11">
|
||||
<path
|
||||
d="m 250.908,330.267 -57.782,84.738 -12.188,-8.311 63.864,-93.657 c 1.372,-2.013 3.651,-3.218 6.087,-3.221 2.437,-0.002 4.717,1.199 6.093,3.21 l 64.012,93.51 -12.173,8.333 z"
|
||||
fill="#67ea94"
|
||||
id="path10" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0.582378,0,0,1.01579,485.019,-211.182)"
|
||||
id="g12">
|
||||
<path
|
||||
d="m 87.642,581.398 67.115,-98.421 -12.119,-8.264 -67.115,98.421 z"
|
||||
fill="#67ea94"
|
||||
id="path11" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<!-- Question mark with glow -->
|
||||
<!-- Control area background -->
|
||||
<rect
|
||||
x="60.289543"
|
||||
y="137"
|
||||
width="112"
|
||||
height="60"
|
||||
rx="6"
|
||||
fill="#0f0f14"
|
||||
id="rect14" />
|
||||
<!-- D-pad style navigation -->
|
||||
<g
|
||||
transform="translate(88.289544,167)"
|
||||
id="g17">
|
||||
<circle
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="22"
|
||||
fill="#1a1a20"
|
||||
stroke="#2a2a32"
|
||||
stroke-width="1"
|
||||
id="circle14" />
|
||||
<path
|
||||
d="M -6,-16 H 6 V -6 H -6 Z"
|
||||
fill="#2a2a32"
|
||||
rx="1"
|
||||
id="path14" />
|
||||
<path
|
||||
d="M -6,6 H 6 V 16 H -6 Z"
|
||||
fill="#2a2a32"
|
||||
rx="1"
|
||||
id="path15" />
|
||||
<path
|
||||
d="M -16,-6 H -6 V 6 h -10 z"
|
||||
fill="#2a2a32"
|
||||
rx="1"
|
||||
id="path16" />
|
||||
<path
|
||||
d="M 6,-6 H 16 V 6 H 6 Z"
|
||||
fill="#2a2a32"
|
||||
rx="1"
|
||||
id="path17" />
|
||||
<circle
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="5"
|
||||
fill="#3a3a42"
|
||||
id="circle17" />
|
||||
</g>
|
||||
<!-- Action button (glowing green) -->
|
||||
<circle
|
||||
cx="144.28955"
|
||||
cy="167"
|
||||
r="14"
|
||||
fill="#1a2a1f"
|
||||
stroke="#67ea94"
|
||||
stroke-width="2"
|
||||
id="circle18" />
|
||||
<circle
|
||||
cx="144.28955"
|
||||
cy="167"
|
||||
r="8"
|
||||
fill="#67ea94"
|
||||
filter="url(#glow)"
|
||||
id="circle19" />
|
||||
<!-- Status LED -->
|
||||
<circle
|
||||
cx="170.26186"
|
||||
cy="25.547974"
|
||||
r="3"
|
||||
fill="#67ea94"
|
||||
opacity="0.8"
|
||||
filter="url(#glow)"
|
||||
id="circle20" />
|
||||
<!-- Side buttons -->
|
||||
<!-- USB port indication at bottom -->
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,202 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import type { createDeviceScene } from '@/graphics/deviceScene'
|
||||
import { deviceSpecs } from '@/graphics/deviceModels'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
model: string
|
||||
label: string
|
||||
fallback?: string
|
||||
fillViewport?: boolean
|
||||
glassContainer?: boolean
|
||||
controls?: boolean
|
||||
/** Development-only gallery controls. Never rendered in production builds. */
|
||||
devBrowse?: boolean
|
||||
}>(), { fallback: '', fillViewport: false, glassContainer: false, controls: true, devBrowse: false })
|
||||
const host = ref<HTMLElement>()
|
||||
const frame = ref<HTMLElement>()
|
||||
const ready = ref(false)
|
||||
const failed = ref(false)
|
||||
const paused = ref(false)
|
||||
const reducedMotion = ref(false)
|
||||
const currentModel = ref(props.model)
|
||||
const devModelIds = Object.keys(deviceSpecs)
|
||||
const devBrowseEnabled = import.meta.env.DEV && props.devBrowse
|
||||
const dragging = ref(false)
|
||||
const remainingHeight = ref('360px')
|
||||
let view: ReturnType<typeof createDeviceScene> | undefined
|
||||
let observer: IntersectionObserver | undefined
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
let motion: MediaQueryList | undefined
|
||||
let visible = false
|
||||
let mounted = false
|
||||
let generation = 0
|
||||
let raf = 0
|
||||
let previous = 0
|
||||
let lastDraw = 0
|
||||
let pointer: number | undefined
|
||||
let lastX = 0
|
||||
let lastY = 0
|
||||
|
||||
function devModelLabel(id: string) { return deviceSpecs[id]?.name ?? id }
|
||||
function browseModel(offset: number) {
|
||||
if (!devBrowseEnabled || !view) return
|
||||
const index = devModelIds.indexOf(currentModel.value)
|
||||
const next = devModelIds[(index + offset + devModelIds.length) % devModelIds.length]!
|
||||
currentModel.value = next
|
||||
view.setModel(next)
|
||||
view.render()
|
||||
}
|
||||
|
||||
function stop() { cancelAnimationFrame(raf); raf = 0; previous = 0; lastDraw = 0 }
|
||||
function canSpin() { return view && visible && !document.hidden && !paused.value && !motion?.matches && !dragging.value }
|
||||
function animate(now: number) {
|
||||
raf = 0
|
||||
if (!canSpin()) { previous = 0; return }
|
||||
// Cap to 30fps; complete a turn in about a minute, independent of refresh rate.
|
||||
if (now - lastDraw >= 32) {
|
||||
view!.rotation.y += previous ? Math.min((now - previous) / 1000, .1) * .105 : 0
|
||||
previous = now; lastDraw = now; view!.render()
|
||||
}
|
||||
raf = requestAnimationFrame(animate)
|
||||
}
|
||||
function sync() { reducedMotion.value = !!motion?.matches; stop(); if (canSpin()) raf = requestAnimationFrame(animate) }
|
||||
function layout() {
|
||||
if (props.fillViewport && frame.value) {
|
||||
// Use the layout position, not its changing scrolled screen position.
|
||||
let scroll = 0
|
||||
for (let el = frame.value.parentElement; el; el = el.parentElement) scroll += el.scrollTop
|
||||
const top = frame.value.getBoundingClientRect().top + scroll
|
||||
// Keep a visible page gutter below the showcase; the frame sits inside a
|
||||
// glass card whose bottom margin must remain visible in the viewport.
|
||||
remainingHeight.value = `${Math.max(260, window.innerHeight - top - 88)}px`
|
||||
}
|
||||
view?.resize()
|
||||
}
|
||||
function disposeScene() {
|
||||
stop(); ready.value = false
|
||||
view?.canvas.removeEventListener('webglcontextlost', contextLost)
|
||||
view?.dispose(); view = undefined
|
||||
}
|
||||
function contextLost(event: Event) {
|
||||
event.preventDefault(); failed.value = true; disposeScene()
|
||||
}
|
||||
async function init() {
|
||||
if (!mounted || !visible || view || failed.value) return
|
||||
const token = ++generation
|
||||
try {
|
||||
const { createDeviceScene } = await import('@/graphics/deviceScene')
|
||||
if (!mounted || token !== generation || !host.value) return
|
||||
view = createDeviceScene(host.value, props.model)
|
||||
view.canvas.addEventListener('webglcontextlost', contextLost)
|
||||
ready.value = true; layout(); sync()
|
||||
} catch {
|
||||
if (token === generation) { failed.value = true; disposeScene() }
|
||||
}
|
||||
}
|
||||
function down(event: PointerEvent) {
|
||||
if (!view || (event.pointerType === 'mouse' && event.button !== 0) || pointer !== undefined) return
|
||||
pointer = event.pointerId; lastX = event.clientX; lastY = event.clientY
|
||||
dragging.value = true; stop(); host.value?.setPointerCapture(pointer)
|
||||
}
|
||||
function move(event: PointerEvent) {
|
||||
if (pointer !== event.pointerId || !view) return
|
||||
view.rotation.y += (event.clientX - lastX) * .012
|
||||
// Touch preserves vertical page scrolling; mouse can also tilt the model.
|
||||
if (event.pointerType === 'mouse') view.rotation.x = Math.max(-.65, Math.min(.65, view.rotation.x + (event.clientY - lastY) * .008))
|
||||
lastX = event.clientX; lastY = event.clientY; view.render()
|
||||
}
|
||||
function up() { pointer = undefined; dragging.value = false; sync() }
|
||||
function key(event: KeyboardEvent) {
|
||||
if (!view || !['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) return
|
||||
event.preventDefault(); paused.value = true; stop()
|
||||
if (event.key === 'Home') view.rotation.set(props.model === 'beryl-ax' ? .27 : -.08, -.4, 0)
|
||||
else if (event.key === 'End') view.rotation.set(props.model === 'beryl-ax' ? .27 : -.08, Math.PI - .4, 0)
|
||||
else if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') view.rotation.y += event.key === 'ArrowLeft' ? -.2 : .2
|
||||
else view.rotation.x = Math.max(-.65, Math.min(.65, view.rotation.x + (event.key === 'ArrowUp' ? -.12 : .12)))
|
||||
view.render()
|
||||
}
|
||||
watch(paused, sync)
|
||||
watch(() => props.model, () => {
|
||||
generation++; up()
|
||||
currentModel.value = props.model
|
||||
if (view) {
|
||||
try { view.setModel(props.model) } catch { failed.value = true; disposeScene() }
|
||||
} else { failed.value = false; void init() }
|
||||
})
|
||||
onMounted(() => {
|
||||
mounted = true
|
||||
if (!window.matchMedia || !window.IntersectionObserver || !window.ResizeObserver) {
|
||||
failed.value = true
|
||||
return
|
||||
}
|
||||
motion = window.matchMedia('(prefers-reduced-motion: reduce)')
|
||||
motion.addEventListener('change', sync)
|
||||
document.addEventListener('visibilitychange', sync)
|
||||
window.addEventListener('resize', layout)
|
||||
resizeObserver = new ResizeObserver(layout)
|
||||
if (host.value) resizeObserver.observe(host.value)
|
||||
if (frame.value?.parentElement) resizeObserver.observe(frame.value.parentElement)
|
||||
observer = new IntersectionObserver(entries => {
|
||||
visible = entries.some(entry => entry.isIntersecting)
|
||||
if (visible) void init()
|
||||
sync()
|
||||
})
|
||||
if (frame.value) observer.observe(frame.value)
|
||||
layout()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
mounted = false; generation++; observer?.disconnect(); resizeObserver?.disconnect()
|
||||
motion?.removeEventListener('change', sync)
|
||||
document.removeEventListener('visibilitychange', sync); window.removeEventListener('resize', layout)
|
||||
disposeScene()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<figure ref="frame" class="device-model" :class="{ 'device-model--viewport': fillViewport, 'device-model--glass': glassContainer }"
|
||||
:style="{ '--device-height': remainingHeight }" :data-model="model" :data-ready="ready">
|
||||
<div ref="host" class="device-model__stage" :class="{ 'is-dragging': dragging }"
|
||||
:tabindex="ready ? 0 : -1" role="group" :aria-label="`${label}. Drag or use arrow keys to rotate. Home shows the front; End shows the back.`"
|
||||
@pointerdown="down" @pointermove="move" @pointerup="up" @pointercancel="up" @lostpointercapture="up" @keydown="key">
|
||||
<img v-if="!ready && fallback" :src="fallback" :alt="label" class="device-model__fallback" />
|
||||
<div v-else-if="!ready" class="device-model__placeholder" role="status" aria-label="Loading 3D illustration">
|
||||
<span class="device-model__loader" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<figcaption v-if="controls || devBrowseEnabled" class="device-model__caption">
|
||||
<span>{{ failed ? label : (devBrowseEnabled ? devModelLabel(currentModel) : 'Drag to explore') }}</span>
|
||||
<template v-if="devBrowseEnabled">
|
||||
<button type="button" aria-label="Previous 3D model" @click="browseModel(-1)">Back</button>
|
||||
<span class="device-model__counter">{{ devModelIds.indexOf(currentModel) + 1 }} / {{ devModelIds.length }}</span>
|
||||
<button type="button" aria-label="Next 3D model" @click="browseModel(1)">Next</button>
|
||||
</template>
|
||||
<button v-if="ready && !reducedMotion" type="button" :aria-label="paused ? 'Resume automatic rotation' : 'Pause automatic rotation'"
|
||||
:aria-pressed="paused" @click="paused = !paused">{{ paused ? 'Rotate' : 'Pause' }}</button>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.device-model { position: relative; width: 100%; height: 240px; margin: 0; isolation: isolate; }
|
||||
.device-model--glass { padding: 16px 18px 12px; border: 1px solid rgba(255,255,255,.13); border-radius: 14px; background: linear-gradient(135deg, rgba(255,255,255,.075), rgba(255,255,255,.025)); box-shadow: inset 0 1px 0 rgba(255,255,255,.045), 0 18px 55px rgba(0,0,0,.14); }
|
||||
.device-model--glass::after { content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; background: linear-gradient(120deg, rgba(255,255,255,.1), transparent 34%, transparent 72%, rgba(255,255,255,.035)); opacity: .45; }
|
||||
.device-model::before { content: ''; position: absolute; inset: 14% 9%; background: radial-gradient(ellipse, rgba(110,167,180,.09), transparent 68%); pointer-events: none; z-index: -1; }
|
||||
.device-model__stage { position: absolute; inset: 0 0 28px; cursor: grab; touch-action: pan-y; border-radius: 16px; }
|
||||
.device-model__stage.is-dragging { cursor: grabbing; }
|
||||
.device-model__stage:focus-visible { outline: 1px solid rgba(255,255,255,.5); outline-offset: 2px; }
|
||||
.device-model__stage :deep(canvas) { display: block; width: 100%; height: 100%; }
|
||||
.device-model__fallback { width: 100%; height: 100%; object-fit: contain; padding: 22px; }
|
||||
.device-model__placeholder { height: 100%; display: grid; place-items: center; }
|
||||
.device-model__loader { display: flex; align-items: center; gap: 8px; opacity: .7; }
|
||||
.device-model__loader i { display: block; width: 7px; height: 7px; border-radius: 999px; background: rgba(140,228,213,.75); animation: device-model-pulse 1.1s ease-in-out infinite; }
|
||||
.device-model__loader i:nth-child(2) { animation-delay: .14s; }
|
||||
.device-model__loader i:nth-child(3) { animation-delay: .28s; }
|
||||
@keyframes device-model-pulse { 0%, 60%, 100% { transform: scale(.65); opacity: .35; } 30% { transform: scale(1); opacity: 1; } }
|
||||
@media (prefers-reduced-motion: reduce) { .device-model__loader i { animation: none; opacity: .65; } }
|
||||
.device-model__caption { position: absolute; inset: auto 0 0; display: flex; justify-content: center; align-items: center; gap: 14px; color: rgba(255,255,255,.4); font-size: 11px; }
|
||||
.device-model__caption button { padding: 4px 9px; color: rgba(255,255,255,.65); border: 1px solid rgba(255,255,255,.12); border-radius: 20px; background: transparent; cursor: pointer; }
|
||||
.device-model__counter { color: rgba(255,255,255,.3); font-variant-numeric: tabular-nums; }
|
||||
.device-model--viewport { height: 290px; }
|
||||
@media (min-width: 1024px) { .device-model--viewport { height: var(--device-height); min-height: 260px; } }
|
||||
</style>
|
||||
@@ -94,6 +94,7 @@ function goalIcon(icon: string): string {
|
||||
lightning: '⚡',
|
||||
identity: '🔑',
|
||||
backup: '💾',
|
||||
phone: '📱',
|
||||
}
|
||||
return icons[icon] || '📦'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
const props = withDefaults(defineProps<{ expanded?: boolean; kind?: 'router' | 'phone' }>(), { expanded: false, kind: 'router' })
|
||||
const emit = defineEmits<{ open: []; close: [] }>()
|
||||
|
||||
interface HardwareListing {
|
||||
id: string
|
||||
pubkey: string
|
||||
title: string
|
||||
summary: string
|
||||
content: string
|
||||
location: string
|
||||
status: string
|
||||
image: string
|
||||
price?: { amount: string; currency: string; frequency?: string }
|
||||
relay: string
|
||||
}
|
||||
|
||||
const relayUrls = ref<string[]>([])
|
||||
const DEV_ROUTER_LISTINGS: HardwareListing[] = [
|
||||
{ id: 'dev-beryl-ax', pubkey: 'archipelago-demo-seller', title: 'GL.iNet Beryl AX (GL-MT3000)', summary: 'Wi‑Fi 6 travel router with OpenWrt, WireGuard and mesh-ready firmware.', content: '', location: 'London, UK', status: 'active', image: '/assets/img/router-mockups/beryl-ax.webp', price: { amount: '89', currency: 'EUR' }, relay: 'development mock' },
|
||||
{ id: 'dev-flint-2', pubkey: 'openwrt-demo-seller', title: 'GL.iNet Flint 2', summary: 'Dual 2.5G Ethernet OpenWrt router for a home gateway or node controller.', content: '', location: 'Berlin, DE', status: 'active', image: '/assets/img/router-mockups/flint-2.webp', price: { amount: '159', currency: 'EUR' }, relay: 'development mock' },
|
||||
{ id: 'dev-nano-pi', pubkey: 'community-demo-seller', title: 'NanoPi R5S OpenWrt gateway', summary: 'Compact RK3568 gateway with three Ethernet ports and community support.', content: '', location: 'Amsterdam, NL', status: 'active', image: '/assets/img/router-mockups/nanopi-r5s.webp', price: { amount: '74', currency: 'EUR' }, relay: 'development mock' },
|
||||
]
|
||||
const DEV_PHONE_LISTINGS: HardwareListing[] = [
|
||||
{ id: 'dev-pixel-9', pubkey: 'privacy-demo-seller', title: 'Pixel 9 · GrapheneOS ready', summary: 'Unlocked Pixel with a long update runway and a fresh install path.', content: '', location: 'London, UK', status: 'active', image: '/assets/img/phone-mockups/pixel-9.webp', price: { amount: '499', currency: 'EUR' }, relay: 'development mock' },
|
||||
{ id: 'dev-pixel-9a', pubkey: 'mobile-demo-seller', title: 'Pixel 9a · unlocked', summary: 'Compact everyday Pixel. Check the bootloader can be unlocked before buying.', content: '', location: 'Berlin, DE', status: 'active', image: '/assets/img/phone-mockups/pixel-9a.webp', price: { amount: '339', currency: 'EUR' }, relay: 'development mock' },
|
||||
{ id: 'dev-pixel-10', pubkey: 'phone-demo-seller', title: 'Pixel 10 · GrapheneOS ready', summary: 'A newer Pixel for private, independent mobile computing.', content: '', location: 'Amsterdam, NL', status: 'active', image: '/assets/img/phone-mockups/pixel-10.webp', price: { amount: '699', currency: 'EUR' }, relay: 'development mock' },
|
||||
]
|
||||
const devListings = computed(() => props.kind === 'phone' ? DEV_PHONE_LISTINGS : DEV_ROUTER_LISTINGS)
|
||||
const topicTags = computed(() => props.kind === 'phone' ? ['pixel'] : ['openwrt', 'router', 'gl-inet', 'beryl', 'flint'])
|
||||
const open = ref(false)
|
||||
const carousel = ref<HTMLElement>()
|
||||
const listings = ref<HardwareListing[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
let requestId = 0
|
||||
let sockets: WebSocket[] = []
|
||||
|
||||
function tag(event: { tags?: string[][] }, name: string): string {
|
||||
return event.tags?.find(item => item[0] === name)?.[1]?.trim() ?? ''
|
||||
}
|
||||
function imageTag(event: { tags?: string[][] }): string {
|
||||
return event.tags?.find(item => item[0] === 'image')?.[1]?.trim() ?? ''
|
||||
}
|
||||
function matchesHardware(item: { title: string; summary: string; content: string; tags?: string[][] }) {
|
||||
const tags = item.tags?.flat().join(' ') ?? ''
|
||||
const text = `${item.title} ${item.summary} ${item.content} ${tags}`
|
||||
return props.kind === 'phone'
|
||||
? /\bpixel\b/i.test(text)
|
||||
: /openwrt|gl[- .]?inet|beryl|flint|nanopi|gl-mt3000|gl-mt6000/i.test(text)
|
||||
}
|
||||
function parse(event: {
|
||||
id?: string; pubkey?: string; content?: string; tags?: string[][]
|
||||
}, relay: string): HardwareListing | null {
|
||||
if (!event.id || !event.pubkey || !matchesHardware({ title: tag(event, 'title'), summary: tag(event, 'summary'), content: event.content ?? '', tags: event.tags })) return null
|
||||
const price = event.tags?.find(item => item[0] === 'price')
|
||||
return {
|
||||
id: event.id,
|
||||
pubkey: event.pubkey,
|
||||
title: tag(event, 'title') || (props.kind === 'phone' ? 'Pixel phone' : 'OpenWrt router'),
|
||||
summary: tag(event, 'summary') || (event.content ?? '').split('\n')[0]?.slice(0, 150) || 'Hardware listing',
|
||||
content: event.content ?? '',
|
||||
location: tag(event, 'location'),
|
||||
status: tag(event, 'status') || 'active',
|
||||
image: imageTag(event),
|
||||
price: price?.[1] && price[2] ? { amount: price[1], currency: price[2], frequency: price[3] } : undefined,
|
||||
relay,
|
||||
}
|
||||
}
|
||||
function closeSockets() {
|
||||
sockets.forEach(socket => socket.close())
|
||||
sockets = []
|
||||
}
|
||||
async function search() {
|
||||
const run = ++requestId
|
||||
closeSockets(); loading.value = true; error.value = ''
|
||||
try {
|
||||
const configured = await rpcClient.call<{ relays?: Array<{ url?: string; enabled?: boolean }> }>({ method: 'nostr.list-relays', params: {} })
|
||||
relayUrls.value = (configured.relays ?? [])
|
||||
.filter(relay => relay.enabled !== false && typeof relay.url === 'string' && /^wss?:\/\//.test(relay.url))
|
||||
.map(relay => relay.url!.replace(/\/$/, ''))
|
||||
} catch {
|
||||
relayUrls.value = []
|
||||
}
|
||||
if (run !== requestId) return
|
||||
if (!relayUrls.value.length) {
|
||||
if (import.meta.env.DEV) {
|
||||
listings.value = devListings.value
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
loading.value = false
|
||||
error.value = 'No Archipelago Nostr relays are configured. Add a relay in Settings → Nostr Relays and try again.'
|
||||
return
|
||||
}
|
||||
const found = new Map<string, HardwareListing>()
|
||||
await Promise.all(relayUrls.value.map(relay => new Promise<void>(resolve => {
|
||||
let settled = false
|
||||
const finish = () => { if (!settled) { settled = true; resolve() } }
|
||||
let socket: WebSocket
|
||||
try { socket = new WebSocket(relay) } catch { finish(); return }
|
||||
sockets.push(socket)
|
||||
const timer = window.setTimeout(() => { socket.close(); finish() }, 8000)
|
||||
socket.addEventListener('open', () => {
|
||||
socket.send(JSON.stringify(['REQ', `archy-${props.kind}-${run}`, {
|
||||
kinds: [30402],
|
||||
'#t': topicTags.value,
|
||||
limit: 100,
|
||||
}, {
|
||||
kinds: [30402],
|
||||
limit: 200,
|
||||
}]))
|
||||
})
|
||||
socket.addEventListener('message', message => {
|
||||
try {
|
||||
const packet = JSON.parse(String(message.data)) as [string, string, { id?: string; pubkey?: string; content?: string; tags?: string[][] }]
|
||||
if (packet[0] === 'EVENT') {
|
||||
const item = parse(packet[2], relay)
|
||||
if (item && item.status !== 'sold') found.set(item.id, item)
|
||||
}
|
||||
if (packet[0] === 'EOSE') { window.clearTimeout(timer); socket.close(); finish() }
|
||||
} catch { /* Ignore malformed relay frames. */ }
|
||||
})
|
||||
socket.addEventListener('error', () => { window.clearTimeout(timer); finish() })
|
||||
socket.addEventListener('close', () => { window.clearTimeout(timer); finish() })
|
||||
})))
|
||||
if (run !== requestId) return
|
||||
listings.value = found.size ? [...found.values()] : import.meta.env.DEV ? devListings.value : []
|
||||
loading.value = false
|
||||
if (!listings.value.length) error.value = `No active ${props.kind === 'phone' ? 'Pixel phone' : 'OpenWrt router'} listings found on your configured relays.`
|
||||
}
|
||||
function openShop() {
|
||||
open.value = true
|
||||
if (import.meta.env.DEV) listings.value = devListings.value
|
||||
emit('open')
|
||||
void search()
|
||||
}
|
||||
function closeShop() { requestId++; open.value = false; closeSockets(); emit('close') }
|
||||
function moveCarousel(direction: number) {
|
||||
const list = carousel.value
|
||||
const card = list?.querySelector<HTMLElement>('.router-marketplace__card')
|
||||
if (list && card) list.scrollBy({ left: direction * (card.offsetWidth + 16), behavior: 'smooth' })
|
||||
}
|
||||
function formatPrice(item: HardwareListing) {
|
||||
if (!item.price) return 'Price on request'
|
||||
return `${item.price.amount} ${item.price.currency.toUpperCase()}${item.price.frequency ? ` / ${item.price.frequency}` : ''}`
|
||||
}
|
||||
onBeforeUnmount(() => { requestId++; closeSockets() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="router-marketplace lg:mt-0" :class="{ 'router-marketplace--expanded': props.expanded }" aria-labelledby="router-marketplace-title">
|
||||
<div class="glass-card p-5 md:p-6">
|
||||
<template v-if="!open">
|
||||
<p class="text-xs uppercase tracking-[.18em] text-white/40">{{ props.kind === 'phone' ? 'Private mobile' : 'OpenWrt gateway' }}</p>
|
||||
<h2 id="router-marketplace-title" class="text-base font-semibold text-white mt-1">{{ props.kind === 'phone' ? 'Ready for a phone that answers to you?' : 'Not got an OpenWrt router?' }}</h2>
|
||||
<p class="text-sm text-white/55 mt-2 max-w-2xl">{{ props.kind === 'phone' ? 'Choose an unlockable Pixel. Install GrapheneOS. Lock the bootloader for verified boot.' : 'Browse community-published router listings from Nostr marketplaces. We’ll add Archipelago checkout after discovery is in place.' }}</p>
|
||||
<button type="button" class="glass-button router-marketplace__cta mt-4" @click="openShop">
|
||||
{{ props.kind === 'phone' ? 'Buy phone' : 'Buy router' }}
|
||||
<span aria-hidden="true">→</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3 mb-2">
|
||||
<div>
|
||||
<h2 id="router-marketplace-title" class="text-base font-semibold text-white">{{ props.kind === 'phone' ? 'Pixel phones for sale' : 'OpenWrt routers for sale' }}</h2>
|
||||
<p class="text-xs text-white/50 mt-1">{{ listings[0]?.relay === 'development mock' ? 'Development preview · conceptual images and sample prices' : 'Live NIP-99 listings from Archipelago relays.' }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" class="router-marketplace__nav" :aria-label="`Previous ${props.kind} listing`" @click="moveCarousel(-1)">‹</button>
|
||||
<button type="button" class="router-marketplace__nav" :aria-label="`Next ${props.kind} listing`" @click="moveCarousel(1)">›</button>
|
||||
<button type="button" class="text-xs text-white/45 hover:text-white ml-2" @click="closeShop">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading && !listings.length" class="sr-only" aria-live="polite">Loading {{ props.kind }} listings</div>
|
||||
<div v-else-if="error && !listings.length" class="text-sm text-white/50 py-5 text-center">{{ error }}</div>
|
||||
<div v-else ref="carousel" class="router-marketplace__results" tabindex="0" :aria-label="`${props.kind === 'phone' ? 'Phone' : 'Router'} listings carousel`">
|
||||
<article v-for="item in listings" :key="item.id" class="router-marketplace__card">
|
||||
<img v-if="item.image" :src="item.image" :alt="item.title" class="router-marketplace__image" loading="lazy" />
|
||||
<div v-else class="router-marketplace__image router-marketplace__image--empty" aria-hidden="true">⌁</div>
|
||||
<div class="router-marketplace__details">
|
||||
<h3 class="text-sm font-medium text-white truncate">{{ item.title }}</h3>
|
||||
<p class="text-xs text-white/55 mt-1 line-clamp-1">{{ item.summary }}</p>
|
||||
<div class="flex flex-wrap gap-x-3 gap-y-1 mt-3 text-[11px] text-white/40">
|
||||
<span v-if="item.location">{{ item.location }}</span>
|
||||
<span>seller {{ item.pubkey.slice(0, 8) }}…</span>
|
||||
</div>
|
||||
<div class="router-marketplace__footer">
|
||||
<strong class="router-marketplace__price">{{ formatPrice(item) }}</strong>
|
||||
<div class="router-marketplace__actions">
|
||||
<button type="button" class="router-marketplace__action" disabled title="Listing details are next">View</button>
|
||||
<button type="button" class="router-marketplace__action router-marketplace__action--buy" disabled title="Checkout is the next step">Buy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.router-marketplace { margin-top: 16px; }
|
||||
.router-marketplace--expanded { grid-column: 1 / -1; width: 100%; min-width: 0; height: 100%; min-height: 0; margin-top: 0; }
|
||||
.router-marketplace--expanded > .glass-card { height: 100%; min-height: 0; display: flex; flex-direction: column; padding: 14px; }
|
||||
.router-marketplace--expanded .router-marketplace__results { flex: 1 1 0; min-height: 0; }
|
||||
.router-marketplace__cta { display: inline-flex; align-items: center; gap: 10px; }
|
||||
.router-marketplace__nav { display: grid; place-items: center; width: 26px; height: 26px; border: 1px solid rgba(255,255,255,.14); border-radius: 50%; color: rgba(255,255,255,.72); font-size: 18px; line-height: 1; }
|
||||
.router-marketplace__nav:hover { background: rgba(255,255,255,.09); }
|
||||
@media (max-width: 1023px) {
|
||||
.router-marketplace__cta { width: 100%; justify-content: center; }
|
||||
/* The dashboard's fixed mobile Back bar must not cover card actions. */
|
||||
.router-marketplace--expanded > .glass-card { padding-bottom: 64px; }
|
||||
}
|
||||
.router-marketplace__results { display: flex; align-items: stretch; gap: 16px; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scroll-snap-type: x mandatory; scrollbar-width: none; mask-image: linear-gradient(to right, #000 0%, #000 calc(100% - 52px), transparent 100%); }
|
||||
.router-marketplace__results::-webkit-scrollbar { display: none; }
|
||||
.router-marketplace__card { display: flex; flex: 0 0 88%; scroll-snap-align: start; flex-direction: column; gap: 12px; min-width: 0; min-height: 0; padding: 12px; border: 1px solid rgba(255,255,255,.09); border-radius: 11px; background: rgba(255,255,255,.035); }
|
||||
.router-marketplace__details { display: flex; flex: 0 0 auto; min-width: 0; flex-direction: column; }
|
||||
.router-marketplace__footer { margin-top: auto; padding-top: 8px; }
|
||||
.router-marketplace__price { display: block; color: rgba(180,244,235,.9); font-size: 18px; }
|
||||
.router-marketplace__actions { display: flex; gap: 8px; margin-top: 8px; }
|
||||
.router-marketplace__action { flex: 1; padding: 8px 12px; border: 1px solid rgba(255,255,255,.14); border-radius: 8px; color: rgba(255,255,255,.55); font-size: 12px; }
|
||||
.router-marketplace__action--buy { border-color: rgba(118,230,191,.35); color: rgba(180,244,235,.9); }
|
||||
@media (min-width: 768px) {
|
||||
.router-marketplace__card { flex-basis: min(360px, 44%); }
|
||||
}
|
||||
.router-marketplace__image { width: 100%; height: 0; min-height: 0; flex: 1 1 0; object-fit: contain; border-radius: 8px; background: linear-gradient(135deg, rgba(112,214,205,.18), rgba(255,255,255,.04)); }
|
||||
.router-marketplace__image--empty { display: grid; place-items: center; color: rgba(121,205,215,.6); font-size: 34px; }
|
||||
</style>
|
||||
@@ -8,25 +8,11 @@
|
||||
>
|
||||
<!-- Step 1: detection graphic + what's currently on the radio -->
|
||||
<div v-if="step === 1" class="text-center">
|
||||
<div class="mx-auto my-2 w-40 h-40 relative">
|
||||
<!-- pulsing signal waves behind the board image -->
|
||||
<svg viewBox="0 0 160 160" class="absolute inset-0 w-full h-full" aria-hidden="true">
|
||||
<g class="mesh-detect-waves" stroke="rgba(251,146,60,0.5)" fill="none" stroke-width="2.5" stroke-linecap="round">
|
||||
<path d="M130 44 a30 30 0 0 1 0 28" />
|
||||
<path d="M138 36 a42 42 0 0 1 0 44" />
|
||||
<path d="M146 28 a54 54 0 0 1 0 60" />
|
||||
</g>
|
||||
</svg>
|
||||
<!-- actual board image (vendored from the Meshtastic web flasher) -->
|
||||
<img
|
||||
:src="deviceImage.image"
|
||||
:alt="deviceImage.label"
|
||||
class="relative w-full h-full object-contain drop-shadow-[0_8px_24px_rgba(251,146,60,0.25)]"
|
||||
@error="imageFailed = true"
|
||||
v-if="!imageFailed"
|
||||
/>
|
||||
<div v-else class="relative w-full h-full flex items-center justify-center text-5xl">📡</div>
|
||||
</div>
|
||||
<DeviceModel
|
||||
:model="deviceImage.model"
|
||||
:label="deviceImage.label"
|
||||
dev-browse
|
||||
/>
|
||||
<p class="text-white text-base font-medium">{{ deviceImage.label }}</p>
|
||||
<p class="text-white/60 text-xs mt-1">
|
||||
{{ deviceImage.exact ? 'Detected' : 'Detected LoRa radio' }} on
|
||||
@@ -306,6 +292,7 @@ import { useMeshStore, type MeshDeviceProbe, type MeshConfigureParams, type Flas
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { LORA_REGIONS, regionByCode, suggestRegionFromLatLon, meshcorePlanFor, RNODE_REGION_PLANS } from '@/utils/loraRegions'
|
||||
import { resolveMeshDeviceImage } from '@/utils/meshDeviceImages'
|
||||
import DeviceModel from '@/components/DeviceModel.vue'
|
||||
|
||||
const mesh = useMeshStore()
|
||||
const appStore = useAppStore()
|
||||
@@ -344,7 +331,6 @@ function stopProbeProgress(done = false) {
|
||||
|
||||
const devicePath = computed(() => mesh.flashFlowPath ?? mesh.undismissedDetectedDevices[0] ?? '')
|
||||
const show = computed(() => !!devicePath.value)
|
||||
const imageFailed = ref(false)
|
||||
const deviceImage = computed(() =>
|
||||
resolveMeshDeviceImage(
|
||||
mesh.status?.detected_device_info?.find(d => d.path === devicePath.value)
|
||||
@@ -417,7 +403,6 @@ watch([show, devicePath], async ([visible]) => {
|
||||
probeError.value = ''
|
||||
probing.value = false
|
||||
stopProbeProgress()
|
||||
imageFailed.value = false
|
||||
openFlashStep()
|
||||
const t = (mesh.status?.device_type ?? '').toLowerCase()
|
||||
if (t === 'meshcore' || t === 'meshtastic' || t === 'reticulum') {
|
||||
@@ -427,7 +412,6 @@ watch([show, devicePath], async ([visible]) => {
|
||||
}
|
||||
step.value = 1
|
||||
error.value = ''
|
||||
imageFailed.value = false
|
||||
form.value.region = suggestedRegion.value?.code ?? mesh.status?.lora_region ?? ''
|
||||
form.value.name = mesh.status?.self_advert_name ?? appStore.serverName ?? ''
|
||||
form.value.channel = mesh.status?.channel_name || 'archipelago'
|
||||
@@ -629,17 +613,3 @@ function closeFlashStep() {
|
||||
step.value = 1
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mesh-detect-waves path {
|
||||
animation: mesh-wave-pulse 2.2s ease-out infinite;
|
||||
opacity: 0;
|
||||
}
|
||||
.mesh-detect-waves path:nth-child(2) { animation-delay: 0.35s; }
|
||||
.mesh-detect-waves path:nth-child(3) { animation-delay: 0.7s; }
|
||||
@keyframes mesh-wave-pulse {
|
||||
0% { opacity: 0; }
|
||||
25% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -67,6 +67,13 @@ export const GOALS: GoalDefinition[] = [
|
||||
route: '/dashboard/setup/external-access', estimatedTime: 'Guided setup', difficulty: 'beginner',
|
||||
steps: [{ id: 'external-access', title: 'Configure access', description: 'Choose and verify each connection.', action: 'configure', isAutomatic: false }],
|
||||
},
|
||||
{
|
||||
id: 'non-surveillance-phone', title: 'Get a non surveillance phone',
|
||||
subtitle: 'Choose an unlockable Pixel and take control with GrapheneOS',
|
||||
icon: 'phone', category: 'identity', requiredApps: [],
|
||||
route: '/dashboard/setup/non-surveillance-phone', estimatedTime: 'Browse phones', difficulty: 'beginner',
|
||||
steps: [{ id: 'choose-private-phone', title: 'Choose a compatible Pixel', description: 'Check that the bootloader can be unlocked, then install GrapheneOS and lock it again for verified boot.', action: 'info', isAutomatic: false }],
|
||||
},
|
||||
{
|
||||
id: 'publish-website', title: 'Publish a website',
|
||||
subtitle: 'Create a website on your node and choose where to publish it',
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# GrapheneOS boot artwork
|
||||
|
||||
`grapheneos-boot-mask.png` is the unmodified boot logo mask from GrapheneOS's
|
||||
Android framework, commit `0f505137b78efaee3058434e88974e2a06e0917c`:
|
||||
|
||||
https://github.com/GrapheneOS/platform_frameworks_base/blob/0f505137b78efaee3058434e88974e2a06e0917c/core/res/assets/images/android-logo-mask.png
|
||||
|
||||
The source commit credits Daniel Micay and smdyv. The framework is distributed
|
||||
under Apache-2.0; the license text is also shipped at
|
||||
`public/assets/fonts/Open_Sans/LICENSE.txt` in this frontend.
|
||||
|
||||
The phone displays a static white phase of the actual GrapheneOS OS boot logo.
|
||||
It does not reproduce Google's separate bootloader / yellow alternate-OS notice.
|
||||
GrapheneOS branding identifies the illustrated operating system; it does not
|
||||
imply endorsement. The phone geometry and materials are original procedural art.
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
@@ -0,0 +1,406 @@
|
||||
import * as THREE from 'three'
|
||||
import { RoundedBoxGeometry } from 'three/addons/geometries/RoundedBoxGeometry.js'
|
||||
import grapheneBootMaskUrl from './assets/grapheneos-boot-mask.png'
|
||||
|
||||
// Original, procedural display models. Dimensions/layouts follow the existing
|
||||
// device illustrations and manufacturer references listed in device-models.md.
|
||||
// These are recognition aids, not mechanical CAD or revision-specific pinouts.
|
||||
type Kind = 'board' | 'echo' | 'deck' | 'card' | 'case' | 'router' | 'phone'
|
||||
interface DeviceSpec {
|
||||
kind: Kind; name: string; w: number; h: number; d: number; color: number
|
||||
screen?: 'oled' | 'paper'; antenna?: boolean; battery?: boolean; pins?: number
|
||||
}
|
||||
export const deviceSpecs: Record<string, DeviceSpec> = {
|
||||
'heltec-v3': { kind: 'board', name: 'HELTEC V3', w: 2.9, h: 1.25, d: .07, color: 0xe8e7df, screen: 'oled', pins: 18 },
|
||||
'heltec_v4': { kind: 'board', name: 'HELTEC V4', w: 2.85, h: 1.2, d: .07, color: 0x181d20, screen: 'oled', pins: 18 },
|
||||
'heltec-wsl-v3': { kind: 'board', name: 'HELTEC WSL', w: 2.85, h: 1, d: .07, color: 0x182225, pins: 18 },
|
||||
'heltec-mesh-node-t114': { kind: 'board', name: 'HELTEC T114', w: 2.65, h: 1.25, d: .08, color: 0xe6e3dc, screen: 'oled' },
|
||||
'tlora-t3s3-v1': { kind: 'board', name: 'LILYGO T3-S3', w: 2.8, h: 1.25, d: .07, color: 0x1b2024, screen: 'oled', antenna: true, pins: 16 },
|
||||
'tlora-v2-1-1_6': { kind: 'board', name: 'LILYGO T-LoRa', w: 2.8, h: 1.2, d: .07, color: 0x25272a, screen: 'oled', antenna: true, pins: 16 },
|
||||
'tbeam': { kind: 'board', name: 'LILYGO T-Beam', w: 3.7, h: 1.2, d: .08, color: 0x242a2d, screen: 'oled', antenna: true, battery: true, pins: 16 },
|
||||
'tbeam-s3-core': { kind: 'board', name: 'T-Beam S3', w: 3.35, h: 1.05, d: .08, color: 0x22272a, screen: 'oled', antenna: true, battery: true },
|
||||
'rak4631': { kind: 'board', name: 'RAK4631', w: 1.4, h: 2.8, d: .08, color: 0x202629, pins: 12 },
|
||||
'seeed-xiao-s3': { kind: 'board', name: 'XIAO ESP32S3', w: 1.25, h: 1.5, d: .08, color: 0x152c28, pins: 7 },
|
||||
'wio-tracker-wm1110': { kind: 'board', name: 'Wio WM1110', w: 1.85, h: 2.7, d: .08, color: 0xb32130, pins: 12 },
|
||||
'rpipicow': { kind: 'board', name: 'Raspberry Pi Pico', w: 3.1, h: 1.2, d: .07, color: 0x16754d, pins: 20 },
|
||||
'diy': { kind: 'board', name: 'RNode', w: 2.8, h: 1.3, d: .07, color: 0x24272b, pins: 19 },
|
||||
't-deck': { kind: 'deck', name: 'LILYGO T-Deck', w: 2, h: 3.2, d: .28, color: 0x25262b, screen: 'oled' },
|
||||
't-echo': { kind: 'echo', name: 'LILYGO', w: 1.7, h: 1.9, d: .5, color: 0xe3e4dc, screen: 'paper', antenna: true },
|
||||
't-echo_plus': { kind: 'echo', name: 'LILYGO', w: 1.9, h: 2.15, d: .65, color: 0x343839, screen: 'paper', antenna: true },
|
||||
'tracker-t1000-e': { kind: 'card', name: 'SenseCAP T1000-E', w: 1.8, h: 2.9, d: .17, color: 0xc6c9c5 },
|
||||
'rak-wismeshtap': { kind: 'case', name: 'WisMesh Tap', w: 2.2, h: 1.7, d: .52, color: 0x33363a, screen: 'oled', antenna: true },
|
||||
'station-g2': { kind: 'case', name: 'Station G2', w: 1.7, h: 2.85, d: .65, color: 0x32363b, screen: 'oled' },
|
||||
'nano-g2-ultra': { kind: 'case', name: 'Nano G2 Ultra', w: 1.7, h: 2.8, d: .5, color: 0x36383d, screen: 'oled' },
|
||||
'thinknode_m1': { kind: 'case', name: 'ThinkNode M1', w: 1.7, h: 2.3, d: .65, color: 0x252c2c, screen: 'paper', antenna: true },
|
||||
'heltec-v3-case': { kind: 'case', name: 'HELTEC V3', w: 2.8, h: 1.25, d: .48, color: 0x13857e, screen: 'oled', antenna: true },
|
||||
'heltec-mesh-node-t114-case': { kind: 'case', name: 'HELTEC T114', w: 1.25, h: 2.4, d: .5, color: 0x10a9c4, screen: 'oled', antenna: true },
|
||||
'rak4631_case': { kind: 'case', name: 'RAK WisBlock', w: 1.4, h: 2.3, d: .55, color: 0xd5dad5, screen: 'oled', antenna: true },
|
||||
'unknown-new': { kind: 'case', name: 'LoRa mesh radio', w: 1.8, h: 2.6, d: .5, color: 0x182623, screen: 'oled' },
|
||||
'beryl-ax': { kind: 'router', name: 'Beryl AX · GL-MT3000', w: 3, h: .8, d: 2.05, color: 0x171b20 },
|
||||
'pixel-graphene': { kind: 'phone', name: 'Pixel 9 Pro · GrapheneOS', w: 1.8, h: 3.82, d: .213, color: 0x08090b },
|
||||
}
|
||||
|
||||
export function createDeviceModel(id: string, invalidate: () => void = () => {}) {
|
||||
const spec = deviceSpecs[id] ?? deviceSpecs['unknown-new']!
|
||||
const root = new THREE.Group()
|
||||
const geometries = new Map<string, THREE.BufferGeometry>()
|
||||
const materials: THREE.Material[] = []
|
||||
const textures: THREE.Texture[] = []
|
||||
let disposed = false
|
||||
const material = (color: number, roughness = .5, metalness = .1) => {
|
||||
const m = new THREE.MeshStandardMaterial({ color, roughness, metalness })
|
||||
materials.push(m); return m
|
||||
}
|
||||
const body = material(spec.color, .43)
|
||||
const black = material(0x10151a, .58)
|
||||
const dark = material(0x03070a, .72)
|
||||
const silver = material(0x9ca9af, .3, .86)
|
||||
const gold = material(0xc9a454, .3, .82)
|
||||
const white = material(0xdce2de, .66)
|
||||
const pcb = material(0x193a30, .6)
|
||||
const glass = material(0x091923, .17, .26)
|
||||
const accent = material(0x76e6bf, .35)
|
||||
accent.emissive.set(0x1b704a); accent.emissiveIntensity = .5
|
||||
const grainCanvas = document.createElement('canvas'); grainCanvas.width = grainCanvas.height = 64
|
||||
const grainContext = grainCanvas.getContext('2d')!
|
||||
const pixels = grainContext.createImageData(64, 64)
|
||||
let seed = 173
|
||||
for (let i = 0; i < pixels.data.length; i += 4) {
|
||||
seed = (seed * 1664525 + 1013904223) >>> 0
|
||||
const value = 200 + seed % 45; pixels.data.set([value, value, value, 255], i)
|
||||
}
|
||||
grainContext.putImageData(pixels, 0, 0)
|
||||
const grain = new THREE.CanvasTexture(grainCanvas)
|
||||
grain.wrapS = grain.wrapT = THREE.RepeatWrapping; grain.repeat.set(7, 7); textures.push(grain)
|
||||
body.roughnessMap = grain; body.bumpMap = grain; body.bumpScale = .0018
|
||||
const geometry = (key: string, build: () => THREE.BufferGeometry) => {
|
||||
if (!geometries.has(key)) geometries.set(key, build())
|
||||
return geometries.get(key)!
|
||||
}
|
||||
function box(w: number, h: number, d: number, x: number, y: number, z: number, m = body, radius = .025, parent = root) {
|
||||
const r = Math.min(radius, w / 3, h / 3, d / 3)
|
||||
const g = geometry(`b${w},${h},${d},${r}`, () => new RoundedBoxGeometry(w, h, d, 2, r))
|
||||
const mesh = new THREE.Mesh(g, m); mesh.position.set(x, y, z); parent.add(mesh); return mesh
|
||||
}
|
||||
function cylinder(r: number, length: number, x: number, y: number, z: number, m = silver, parent = root) {
|
||||
const g = geometry(`c${r},${length}`, () => new THREE.CylinderGeometry(r, r, length, 20))
|
||||
const mesh = new THREE.Mesh(g, m); mesh.position.set(x, y, z); parent.add(mesh); return mesh
|
||||
}
|
||||
function disk(r: number, x: number, y: number, z: number, m = silver) {
|
||||
const mesh = cylinder(r, .018, x, y, z, m); mesh.rotation.x = Math.PI / 2; return mesh
|
||||
}
|
||||
function label(text: string, w: number, h: number, x: number, y: number, z: number, back = false, color = '#dbe4e3') {
|
||||
const canvas = document.createElement('canvas'); canvas.width = 512; canvas.height = 96
|
||||
const ctx = canvas.getContext('2d')!
|
||||
ctx.fillStyle = color; ctx.font = '500 40px monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, 256, 48, 500)
|
||||
const texture = new THREE.CanvasTexture(canvas); texture.colorSpace = THREE.SRGBColorSpace; textures.push(texture)
|
||||
const m = new THREE.MeshBasicMaterial({ map: texture, transparent: true, depthWrite: false }); materials.push(m)
|
||||
const mesh = new THREE.Mesh(geometry(`p${w},${h}`, () => new THREE.PlaneGeometry(w, h)), m)
|
||||
mesh.position.set(x, y, z); if (back) mesh.rotation.y = Math.PI; root.add(mesh); return mesh
|
||||
}
|
||||
function screws(w: number, h: number, z: number) {
|
||||
for (const x of [-w / 2 + .12, w / 2 - .12]) for (const y of [-h / 2 + .12, h / 2 - .12]) {
|
||||
disk(.055, x, y, z, silver)
|
||||
box(.058, .012, .012, x, y, z + Math.sign(z) * .013, dark, .001)
|
||||
}
|
||||
}
|
||||
function antenna(x: number, y: number, length = 1.3) {
|
||||
cylinder(.085, .2, x, y + .1, 0, gold)
|
||||
cylinder(.1, .18, x, y + .24, 0, black)
|
||||
cylinder(.068, length, x, y + .3 + length / 2, 0, black)
|
||||
for (let i = 0; i < 3; i++) cylinder(.074, .02, x, y + length + .1 + i * .05, 0, body)
|
||||
}
|
||||
function screen(w: number, h: number, x: number, y: number, z: number, paper = false) {
|
||||
box(w + .12, h + .12, .065, x, y, z, black)
|
||||
box(w, h, .02, x, y, z + .038, paper ? white : glass, .015)
|
||||
const ink = paper ? '#263834' : '#8ce4d5'
|
||||
label('MESH', w * .65, h * .28, x, y + h * .14, z + .052, false, ink)
|
||||
for (let i = 0; i < 4; i++) box(.028, .045 + .035 * i, .008, x + w * .3 + i * .045, y - h * .32, z + .053, paper ? black : accent, .002)
|
||||
label('LoRa', w * .48, h * .18, x - w * .08, y - h * .2, z + .054, false, ink)
|
||||
}
|
||||
function usb(x: number, y: number, z: number, back = false) {
|
||||
box(.28, .115, .14, x, y, z, silver)
|
||||
box(.22, .063, .015, x, y, z + (back ? -.077 : .077), dark)
|
||||
box(.15, .015, .018, x, y, z + (back ? -.084 : .084), black)
|
||||
}
|
||||
const { w, h, d } = spec
|
||||
if (spec.kind === 'router') {
|
||||
// Beryl AX: two hinged paddles, rear USB-C / WAN / LAN / vertical USB-A.
|
||||
box(w, h, d, 0, 0, 0, body, .25)
|
||||
for (const z of [-1.023, 1.023]) box(2.56, .013, .008, 0, -.12, z, black, .002)
|
||||
label('GL.iNet', .62, .14, 0, .423, .08, false, '#d5e9ed').rotation.x = -Math.PI / 2
|
||||
box(.22, .032, .022, 0, -.115, d / 2 + .012, white)
|
||||
for (const side of [-1, 1]) {
|
||||
const hinge = cylinder(.16, .12, side * 1.57, -.04, .36, body); hinge.rotation.z = Math.PI / 2
|
||||
const paddle = new THREE.Group(); paddle.position.set(side * 1.62, 0, .36); paddle.rotation.z = side * -.1; root.add(paddle)
|
||||
box(.27, 1.93, .19, 0, .86, 0, body, .085, paddle)
|
||||
box(.18, 1.65, .015, 0, .88, .102, body, .005, paddle)
|
||||
for (let i = 0; i < 24; i++) box(.014, .37, .028, side * 1.497, -.035, -.85 + i * .069, dark, .003)
|
||||
for (const z of [-.72, .72]) {
|
||||
cylinder(.14, .08, side * 1.12, -.43, z, black)
|
||||
for (let i = 0; i < 14; i++) box(.038, .012, .45, side * (.35 + i * .055), -.405, z * .6, dark, .002)
|
||||
}
|
||||
}
|
||||
box(2.46, .55, .022, 0, -.015, -1.031, body, .008)
|
||||
usb(1, .04, -1.035, true)
|
||||
label('POWER', .3, .075, 1, .2, -1.057, true)
|
||||
for (const [x, text] of [[.38, '2.5G WAN'], [-.27, 'LAN']] as const) {
|
||||
box(.51, .4, .03, x, -.025, -1.055, silver)
|
||||
box(.45, .32, .03, x, -.025, -1.074, dark)
|
||||
box(.22, .06, .04, x, -.19, -1.08, dark)
|
||||
for (let i = 0; i < 8; i++) box(.019, .08, .014, x - .15 + i * .043, .062, -1.095, gold, .001)
|
||||
label(text, .53, .075, x, .24, -1.06, true)
|
||||
}
|
||||
box(.22, .4, .035, -.93, -.04, -1.06, silver)
|
||||
box(.145, .32, .02, -.93, -.04, -1.086, dark)
|
||||
box(.038, .26, .022, -.92, -.04, -1.1, material(0x287ecc))
|
||||
label('USB 3.0', .42, .075, -.93, .24, -1.06, true)
|
||||
box(.018, .085, .18, -1.508, .14, -.5, black)
|
||||
box(.02, .05, .05, -1.51, .14, -.77, dark)
|
||||
const underside = label('GL-MT3000 / BERYL AX', 1.25, .15, 0, -.413, 0)
|
||||
underside.rotation.x = Math.PI / 2
|
||||
} else if (spec.kind === 'phone') {
|
||||
// Rounded outlines are independent of thickness: a thin RoundedBox clamps
|
||||
// the corner radius to its depth and makes a handset look square.
|
||||
function outline(width: number, height: number, radius: number) {
|
||||
const x = -width / 2, y = -height / 2, r = radius
|
||||
const shape = new THREE.Shape()
|
||||
shape.moveTo(x + r, y); shape.lineTo(x + width - r, y)
|
||||
shape.quadraticCurveTo(x + width, y, x + width, y + r)
|
||||
shape.lineTo(x + width, y + height - r)
|
||||
shape.quadraticCurveTo(x + width, y + height, x + width - r, y + height)
|
||||
shape.lineTo(x + r, y + height)
|
||||
shape.quadraticCurveTo(x, y + height, x, y + height - r)
|
||||
shape.lineTo(x, y + r); shape.quadraticCurveTo(x, y, x + r, y)
|
||||
return shape
|
||||
}
|
||||
function panel(width: number, height: number, depth: number, radius: number, z: number, m: THREE.Material, x = 0, y = 0) {
|
||||
const bevel = Math.min(.012, depth / 4)
|
||||
const g = geometry(`phone${width},${height},${depth},${radius}`, () => {
|
||||
const shape = outline(width - bevel * 2, height - bevel * 2, radius - bevel)
|
||||
const result = new THREE.ExtrudeGeometry(shape, { depth: depth - bevel * 2, bevelEnabled: true, bevelThickness: bevel, bevelSize: bevel, bevelSegments: 3, steps: 1, curveSegments: 12 })
|
||||
result.translate(0, 0, -depth / 2 + bevel)
|
||||
return result
|
||||
})
|
||||
const mesh = new THREE.Mesh(g, m); mesh.position.set(x, y, z); root.add(mesh); return mesh
|
||||
}
|
||||
const frame = material(0x101113, .32, .72)
|
||||
const edge = material(0x24262a, .27, .8)
|
||||
const rear = material(0x08090b, .32, .12)
|
||||
const lensRing = material(0x303238, .25, .78)
|
||||
const lens = material(0x080d16, .12, .3)
|
||||
const lensCoating = material(0x172a38, .16, .45)
|
||||
const seal = material(0x030405, .85)
|
||||
panel(w, h, d, .265, 0, frame)
|
||||
panel(w - .024, h - .024, .022, .255, -d / 2, edge)
|
||||
panel(w - .048, h - .048, .018, .245, -d / 2 - .013, rear)
|
||||
panel(w - .02, h - .02, .02, .255, d / 2, seal)
|
||||
panel(w - .044, h - .044, .012, .244, d / 2 + .011, material(0x050609, .2, .08))
|
||||
const oled = new THREE.MeshBasicMaterial({ color: 0x000000, toneMapped: false }); materials.push(oled)
|
||||
panel(w - .108, h - .108, .004, .216, d / 2 + .02, oled)
|
||||
|
||||
// Unmodified GrapheneOS boot mask, centered at its native Pixel 9 Pro
|
||||
// screen ratio (448 / 1280). Static white phase of the OS boot animation.
|
||||
const logoW = (w - .108) * 448 / 1280, logoH = logoW * 605 / 448
|
||||
const logoGeometry = geometry('graphene-boot-logo', () => new THREE.PlaneGeometry(logoW, logoH))
|
||||
const bootCanvas = document.createElement('canvas'); bootCanvas.width = 448; bootCanvas.height = 605
|
||||
const boot = bootCanvas.getContext('2d')!
|
||||
boot.fillStyle = '#000'; boot.fillRect(0, 0, 448, 605)
|
||||
const bootTexture = new THREE.CanvasTexture(bootCanvas)
|
||||
bootTexture.colorSpace = THREE.SRGBColorSpace; textures.push(bootTexture)
|
||||
new THREE.ImageLoader().load(grapheneBootMaskUrl, image => {
|
||||
if (disposed) return
|
||||
boot.fillStyle = '#f3f3f3'; boot.fillRect(0, 0, 448, 605)
|
||||
boot.drawImage(image, 0, 0); bootTexture.needsUpdate = true; invalidate()
|
||||
})
|
||||
const logoMaterial = new THREE.MeshBasicMaterial({ map: bootTexture, toneMapped: false }); materials.push(logoMaterial)
|
||||
const logo = new THREE.Mesh(logoGeometry, logoMaterial); logo.position.z = d / 2 + .026; root.add(logo)
|
||||
disk(.031, 0, h / 2 - .145, d / 2 + .028, seal)
|
||||
disk(.017, 0, h / 2 - .145, d / 2 + .039, lens)
|
||||
panel(.32, .014, .006, .007, d / 2 + .02, seal, 0, h / 2 - .044)
|
||||
|
||||
// Pixel 9 Pro's raised camera island, glass pill, three layered optics,
|
||||
// flash and temperature sensor; all face outward on the rear.
|
||||
const cameraY = h / 2 - .68
|
||||
panel(1.57, .59, .105, .285, -d / 2 - .058, frame, 0, cameraY)
|
||||
panel(1.51, .53, .018, .255, -d / 2 - .117, edge, 0, cameraY)
|
||||
panel(1.14, .402, .012, .196, -d / 2 - .13, seal, .125, cameraY)
|
||||
for (const x of [-.245, .125, .49]) {
|
||||
disk(.154, x, cameraY, -d / 2 - .142, lensRing)
|
||||
disk(.13, x, cameraY, -d / 2 - .153, dark)
|
||||
disk(.103, x, cameraY, -d / 2 - .164, lens)
|
||||
disk(.064, x, cameraY, -d / 2 - .175, lensCoating)
|
||||
disk(.039, x, cameraY, -d / 2 - .186, dark)
|
||||
disk(.018, x - .029, cameraY + .038, -d / 2 - .197, material(0x46556c, .18, .3))
|
||||
}
|
||||
disk(.069, -.582, cameraY + .11, -d / 2 - .139, material(0xc8c4ae, .5))
|
||||
disk(.033, -.582, cameraY - .11, -d / 2 - .139, lens)
|
||||
disk(.009, -.45, cameraY - .12, -d / 2 - .14, seal)
|
||||
// Black buttons, antenna seams, bottom USB-C opening and speaker holes.
|
||||
box(.038, .28, .09, w / 2 + .012, .74, 0, frame, .012)
|
||||
box(.038, .45, .09, w / 2 + .012, .19, 0, frame, .012)
|
||||
for (const side of [-1, 1]) for (const y of [-1.35, 1.36]) box(.008, .027, d - .035, side * (w / 2 + .001), y, 0, seal, .002)
|
||||
const port = panel(.245, .071, .009, .032, 0, edge)
|
||||
port.rotation.x = Math.PI / 2; port.position.set(0, -h / 2 - .002, 0)
|
||||
const portHole = panel(.213, .047, .01, .021, 0, seal)
|
||||
portHole.rotation.x = Math.PI / 2; portHole.position.set(0, -h / 2 - .008, 0)
|
||||
box(.145, .008, .012, 0, -h / 2 - .015, 0, frame, .003)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const hole = disk(.016, 0, 0, 0, seal)
|
||||
hole.rotation.x = 0; hole.position.set(.34 + i * .067, -h / 2 - .003, 0)
|
||||
}
|
||||
const mic = disk(.015, 0, 0, 0, seal); mic.rotation.x = 0; mic.position.set(-.4, -h / 2 - .003, 0)
|
||||
} else if (spec.kind === 'board') {
|
||||
box(w, h, d, 0, 0, 0, body, .06)
|
||||
// Plated through holes, copper traces, solder joints and populated reverse.
|
||||
const horizontal = w > h; const count = spec.pins ?? 10
|
||||
for (const side of [-1, 1]) for (let i = 0; i < count; i++) {
|
||||
const a = ((i + .5) / count - .5) * ((horizontal ? w : h) - .28)
|
||||
const x = horizontal ? a : side * (w / 2 - .08)
|
||||
const y = horizontal ? side * (h / 2 - .08) : a
|
||||
for (const face of [-1, 1]) {
|
||||
disk(.037, x, y, face * (d / 2 + .008), gold)
|
||||
disk(.018, x, y, face * (d / 2 + .021), dark)
|
||||
}
|
||||
}
|
||||
const heltec = id.startsWith('heltec')
|
||||
const shieldX = spec.screen ? w * .28 : w * .05
|
||||
if (!heltec || !spec.screen) {
|
||||
box(w * .25, h * .43, .11, shieldX, .05, .095, silver)
|
||||
label(spec.name.split(' ')[0]!, w * .23, .08, shieldX, .06, .158, false, '#354047')
|
||||
} else {
|
||||
box(w * .32, h * .43, .11, shieldX, .05, -.12, silver)
|
||||
label('ESP32 · LoRa', w * .29, .08, shieldX, .06, -.18, true, '#354047')
|
||||
}
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const x = -w * .38 + (i % 6) * w * .14, y = -h * .28 + Math.floor(i / 6) * h * .56
|
||||
box(.095, .043, .035, x, y, .065, i % 3 ? black : silver, .003)
|
||||
box(.15, .023, .008, x, y, -.05, gold, .002)
|
||||
box(.022, .18, .008, x, y + .07, -.047, pcb, .001)
|
||||
}
|
||||
box(w * .28, h * .34, .055, -w * .15, 0, -.08, black)
|
||||
label(spec.name, w * .65, .12, 0, -h * .31, -.114, true)
|
||||
// SMD passives with metal end caps, short traces and silkscreen outlines.
|
||||
for (const face of [-1, 1]) for (let i = 0; i < 26; i++) {
|
||||
const x = -w * .4 + (i % 13) * w * .065, y = (i < 13 ? -.33 : .33) * h
|
||||
box(.065, .029, .021, x, y, face * .061, i % 4 ? black : material(0x8b7856), .002)
|
||||
for (const end of [-1, 1]) box(.013, .032, .024, x + end * .032, y, face * .063, silver, .001)
|
||||
box(.09, .007, .004, x, y + .03, face * .043, white, .001)
|
||||
}
|
||||
if (spec.screen) screen(w * (heltec ? .52 : .36), h * .51, w * (heltec ? .15 : -.15), 0, .145)
|
||||
else {
|
||||
box(w * .23, h * .28, .055, -w * .26, .03, .1, black)
|
||||
for (let i = 0; i < 6; i++) box(.028, h * .32, .015, -w * .36 + i * .04, .03, .085, silver, .001)
|
||||
}
|
||||
// USB connector faces outward at the short edge.
|
||||
const port = new THREE.Group(); root.add(port)
|
||||
box(.29, .17, .14, 0, 0, 0, silver, .02, port)
|
||||
box(.22, .11, .015, 0, 0, .078, dark, .018, port)
|
||||
if (horizontal) { port.position.set((heltec ? -1 : 1) * (w / 2 + .025), 0, .12); port.rotation.y = (heltec ? -1 : 1) * Math.PI / 2 }
|
||||
else { port.position.set(0, h / 2 + .025, .12); port.rotation.x = -Math.PI / 2 }
|
||||
disk(.055, -w * .4, .12, .076, gold)
|
||||
box(.105, .105, .055, -w * .4, -.14, .09, silver)
|
||||
box(.052, .052, .024, -w * .4, -.14, .13, black)
|
||||
if (spec.antenna) antenna(w * .27, h / 2, 1.25)
|
||||
if (spec.battery) {
|
||||
box(w * .82, .85, .5, 0, 0, -.31, black, .05)
|
||||
const cell = cylinder(.34, w * .7, 0, 0, -.52, material(0x8461a5)); cell.rotation.z = Math.PI / 2
|
||||
for (const x of [-w * .36, w * .36]) { const end = cylinder(.29, .05, x, 0, -.52); end.rotation.z = Math.PI / 2 }
|
||||
}
|
||||
if (id === 'wio-tracker-wm1110') for (const x of [-.66, .66]) for (let i = 0; i < 4; i++) box(.21, .2, .15, x, -.87 + i * .52, .1, white)
|
||||
if (id === 'rak4631') {
|
||||
box(1.08, .96, .035, 0, .45, .18, material(0x187b65))
|
||||
box(.86, .73, .04, 0, .45, .22, silver)
|
||||
label('RAK', .52, .19, 0, .56, .246, false, '#126d95')
|
||||
label('4631', .45, .1, 0, .33, .247, false, '#354047')
|
||||
for (const x of [-.43, 0, .43]) box(.21, .52, .06, x, -.74, .12, black)
|
||||
}
|
||||
} else {
|
||||
box(w, h, d, 0, 0, 0, body, .17)
|
||||
// A recessed rear panel preserves the rounded enclosure silhouette.
|
||||
box(w * .88, h * .91, .025, 0, 0, -d / 2 - .012, body, .008)
|
||||
screws(w * .94, h * .95, -d / 2 - .031)
|
||||
label(spec.name, w * .66, .11, 0, -.2, -d / 2 - .045, true)
|
||||
label('LoRa · wireless', w * .52, .07, 0, -.34, -d / 2 - .045, true)
|
||||
if (spec.kind === 'deck') {
|
||||
screen(w * .79, h * .38, 0, h * .19, d / 2 + .015)
|
||||
const keys = ['QWERTYUIOP', 'ASDFGHJKL', 'ZXCVBNM']
|
||||
keys.forEach((row, r) => [...row].forEach((key, i) => {
|
||||
const x = (i - (row.length - 1) / 2) * .165, y = -.55 - r * .19
|
||||
box(.146, .15, .07, x, y, d / 2 + .03, black, .02)
|
||||
label(key, .065, .06, x, y, d / 2 + .069)
|
||||
}))
|
||||
box(.63, .13, .065, 0, -1.12, d / 2 + .03, black)
|
||||
disk(.11, 0, -.28, d / 2 + .04, silver)
|
||||
disk(.072, 0, -.28, d / 2 + .056, white)
|
||||
label('LILYGO', .48, .09, 0, 1.47, d / 2 + .02)
|
||||
usb(0, -h / 2 + .05, -.015)
|
||||
// Exposed rear PCB on the original T-Deck development board.
|
||||
box(w * .85, h * .83, .045, 0, 0, -d / 2 - .05, pcb)
|
||||
box(.7, .65, .1, .35, .7, -d / 2 - .11, silver)
|
||||
label('ESP32-S3', .55, .08, .35, .7, -d / 2 - .165, true, '#354047')
|
||||
box(1.35, 1.1, .06, 0, -.65, -d / 2 - .09, black)
|
||||
for (let i = 0; i < 8; i++) box(.07, .12, .04, -.65 + i * .18, .04, -d / 2 - .09, gold)
|
||||
} else if (spec.kind === 'echo') {
|
||||
box(w * .49, .42, d * .85, w * .25, h / 2 + .12, 0, body, .08)
|
||||
label('LILYGO', w * .37, .13, w * .25, h / 2 + .14, d * .43, false, '#67b6d8')
|
||||
screen(w * .57, h * .57, 0, -.05, d / 2 + .017, true)
|
||||
screws(w, h, d / 2 + .024)
|
||||
antenna(w * .25, h / 2 + .33, 1.15)
|
||||
for (const y of [-.3, 0, .3]) box(.05, .14, .17, -w / 2 - .02, y, 0, white)
|
||||
usb(0, -h / 2 + .1, -d / 2, true)
|
||||
if (id === 't-echo_plus') {
|
||||
const loop = new THREE.Mesh(geometry('loop', () => new THREE.TorusGeometry(.31, .065, 10, 28, Math.PI)), silver)
|
||||
loop.position.set(-.25, h / 2 + .22, -.08); root.add(loop)
|
||||
}
|
||||
} else if (spec.kind === 'card') {
|
||||
disk(.31, 0, -.3, d / 2 + .015, gold)
|
||||
disk(.24, 0, -.3, d / 2 + .029, body)
|
||||
disk(.06, 0, .92, d / 2 + .018, dark)
|
||||
label('SenseCAP', 1.03, .17, 0, .35, d / 2 + .021, false, '#59645f')
|
||||
box(.46, .085, .025, 0, h / 2 - .12, d / 2 + .008, dark)
|
||||
for (let i = 0; i < 4; i++) disk(.045, -.22 + i * .145, -.95, -d / 2 - .047, gold)
|
||||
} else {
|
||||
if (spec.screen) screen(w * .6, Math.min(h * .3, .7), 0, h * -.04, d / 2 + .025, spec.screen === 'paper')
|
||||
label(spec.name, w * .72, .13, 0, -h * .35, d / 2 + .025)
|
||||
screws(w, h, d / 2 + .016)
|
||||
for (let i = 0; i < 7; i++) box(.045, h * .16, .013, (i - 3) * w * .08, h * .3, d / 2 + .012, dark, .004)
|
||||
usb(0, -h / 2 + .09, -d / 2, true)
|
||||
if (spec.antenna) antenna(w * .25, h / 2, 1.05)
|
||||
if (id === 'unknown-new') {
|
||||
for (const x of [-.48, .48]) disk(.12, x, -.85, d / 2 + .02, accent)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Batch repeated opaque hardware. Every device owns/disposes its resources.
|
||||
function batch(parent: THREE.Object3D) {
|
||||
const groups = new Map<string, THREE.Mesh[]>()
|
||||
for (const child of [...parent.children]) {
|
||||
if (child.children.length) batch(child)
|
||||
if (!(child instanceof THREE.Mesh) || Array.isArray(child.material) || child.material.transparent) continue
|
||||
const key = child.geometry.uuid + child.material.uuid
|
||||
const list = groups.get(key) ?? []; list.push(child); groups.set(key, list)
|
||||
}
|
||||
for (const meshes of groups.values()) if (meshes.length > 1) {
|
||||
const first = meshes[0]!
|
||||
const instance = new THREE.InstancedMesh(first.geometry, first.material, meshes.length)
|
||||
meshes.forEach((mesh, i) => { mesh.updateMatrix(); instance.setMatrixAt(i, mesh.matrix); parent.remove(mesh) })
|
||||
parent.add(instance)
|
||||
}
|
||||
}
|
||||
batch(root)
|
||||
const bounds = new THREE.Box3().setFromObject(root)
|
||||
root.position.sub(bounds.getCenter(new THREE.Vector3()))
|
||||
const pivot = new THREE.Group(); pivot.add(root)
|
||||
const radius = bounds.getBoundingSphere(new THREE.Sphere()).radius
|
||||
root.traverse(object => { object.updateMatrix(); object.matrixAutoUpdate = false })
|
||||
return {
|
||||
pivot, radius, name: spec.name,
|
||||
dispose() {
|
||||
disposed = true
|
||||
root.traverse(object => { if (object instanceof THREE.InstancedMesh) object.dispose() })
|
||||
geometries.forEach(g => g.dispose()); materials.forEach(m => m.dispose()); textures.forEach(t => t.dispose())
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import * as THREE from 'three'
|
||||
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'
|
||||
import { createDeviceModel } from './deviceModels'
|
||||
|
||||
export function createDeviceScene(host: HTMLElement, id: string) {
|
||||
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, powerPreference: 'low-power' })
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.75))
|
||||
renderer.setClearColor(0x000000, 0)
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping
|
||||
renderer.toneMappingExposure = .94
|
||||
renderer.domElement.setAttribute('aria-hidden', 'true')
|
||||
const scene = new THREE.Scene()
|
||||
const camera = new THREE.PerspectiveCamera(33, 1, .1, 100)
|
||||
let disposed = false
|
||||
let model = createDeviceModel(id, render)
|
||||
scene.add(model.pivot)
|
||||
model.pivot.rotation.set(id === 'beryl-ax' ? .27 : -.08, -.4, 0)
|
||||
const studio = new RoomEnvironment()
|
||||
const pmrem = new THREE.PMREMGenerator(renderer)
|
||||
const environment = pmrem.fromScene(studio, .04, .1, 100, { size: 128 })
|
||||
scene.environment = environment.texture; scene.environmentIntensity = 1.05
|
||||
studio.dispose(); pmrem.dispose()
|
||||
scene.add(new THREE.HemisphereLight(0xe4eeff, 0x354052, 2.25))
|
||||
for (const [color, intensity, x, y, z] of [
|
||||
[0xffffff, 1.15, 3, 5, 6], [0x91c6e8, .85, -4, 1, -3], [0xe8c5ac, .65, 4, 0, -4],
|
||||
]) {
|
||||
const light = new THREE.DirectionalLight(color, intensity)
|
||||
light.position.set(x!, y!, z!); scene.add(light)
|
||||
}
|
||||
host.append(renderer.domElement)
|
||||
function render() { if (!disposed) renderer.render(scene, camera) }
|
||||
function resize() {
|
||||
if (disposed) return
|
||||
const { width, height } = host.getBoundingClientRect()
|
||||
if (!width || !height) return
|
||||
renderer.setSize(width, height, false)
|
||||
camera.aspect = width / height
|
||||
const halfFov = Math.atan(Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)) * Math.min(1, camera.aspect))
|
||||
camera.position.set(0, 0, model.radius / Math.sin(halfFov) * 1.09)
|
||||
camera.lookAt(0, 0, 0); camera.updateProjectionMatrix(); render()
|
||||
}
|
||||
resize()
|
||||
return {
|
||||
canvas: renderer.domElement, get rotation() { return model.pivot.rotation }, render, resize,
|
||||
setModel(next: string) {
|
||||
const replacement = createDeviceModel(next, render)
|
||||
scene.remove(model.pivot); model.dispose(); model = replacement
|
||||
model.pivot.rotation.set(next === 'beryl-ax' ? .27 : -.08, -.4, 0)
|
||||
scene.add(model.pivot); resize()
|
||||
},
|
||||
dispose() {
|
||||
if (disposed) return
|
||||
disposed = true; model.dispose(); environment.dispose(); renderer.dispose()
|
||||
renderer.forceContextLoss(); renderer.domElement.remove()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -255,6 +255,11 @@ const router = createRouter({
|
||||
name: 'publish-website',
|
||||
component: () => import('@/views/publishing/PublishingSetup.vue'),
|
||||
},
|
||||
{
|
||||
path: 'setup/non-surveillance-phone',
|
||||
name: 'non-surveillance-phone',
|
||||
component: () => import('@/views/goals/NonSurveillancePhone.vue'),
|
||||
},
|
||||
{
|
||||
path: 'goals/:goalId',
|
||||
name: 'goal-detail',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* Best-effort mapping from a detected serial port's USB identity to a board
|
||||
* image (SVGs vendored from the Meshtastic web-flasher, GPL-3.0 —
|
||||
* github.com/meshtastic/web-flasher, public/img/devices/).
|
||||
* Best-effort mapping from a detected serial port's USB identity to an
|
||||
* original procedural Three.js model.
|
||||
*
|
||||
* Native-USB boards (T-Deck, RAK4631, T1000-E…) report their name in the USB
|
||||
* product string, so those match precisely. UART-bridge boards only identify
|
||||
@@ -18,58 +17,56 @@ export interface DetectedDeviceInfo {
|
||||
}
|
||||
|
||||
export interface MeshDeviceImage {
|
||||
/** Path under public/ */
|
||||
image: string
|
||||
/** Procedural model key in graphics/deviceModels.ts. */
|
||||
model: string
|
||||
/** Human name shown in the modal */
|
||||
label: string
|
||||
/** True when the match is exact (product string), false for chip-level guesses */
|
||||
exact: boolean
|
||||
}
|
||||
|
||||
const IMG = '/assets/img/mesh-devices'
|
||||
|
||||
/** product-string keyword → image (checked in order, case-insensitive) */
|
||||
const PRODUCT_MATCHES: Array<{ match: RegExp; image: string; label: string }> = [
|
||||
{ match: /t-?deck/i, image: `${IMG}/t-deck.svg`, label: 'LILYGO T-Deck' },
|
||||
{ match: /t-?echo\s*plus/i, image: `${IMG}/t-echo_plus.svg`, label: 'LILYGO T-Echo Plus' },
|
||||
{ match: /t-?echo/i, image: `${IMG}/t-echo.svg`, label: 'LILYGO T-Echo' },
|
||||
{ match: /t-?beam\s*s3/i, image: `${IMG}/tbeam-s3-core.svg`, label: 'LILYGO T-Beam S3' },
|
||||
{ match: /t-?beam/i, image: `${IMG}/tbeam.svg`, label: 'LILYGO T-Beam' },
|
||||
{ match: /t3.?s3/i, image: `${IMG}/tlora-t3s3-v1.svg`, label: 'LILYGO T3-S3' },
|
||||
{ match: /t-?lora|tlora/i, image: `${IMG}/tlora-v2-1-1_6.svg`, label: 'LILYGO T-LoRa' },
|
||||
{ match: /rak.?4631|wisblock/i, image: `${IMG}/rak4631.svg`, label: 'RAK WisBlock 4631' },
|
||||
{ match: /wismesh|wistap/i, image: `${IMG}/rak-wismeshtap.svg`, label: 'RAK WisMesh Tap' },
|
||||
{ match: /t1000|tracker.?t1000/i, image: `${IMG}/tracker-t1000-e.svg`, label: 'Seeed Card Tracker T1000-E' },
|
||||
{ match: /xiao/i, image: `${IMG}/seeed-xiao-s3.svg`, label: 'Seeed XIAO S3' },
|
||||
{ match: /wio.?tracker|wm1110/i, image: `${IMG}/wio-tracker-wm1110.svg`, label: 'Seeed Wio Tracker' },
|
||||
{ match: /station\s*g2/i, image: `${IMG}/station-g2.svg`, label: 'B&Q Station G2' },
|
||||
{ match: /nano\s*g2/i, image: `${IMG}/nano-g2-ultra.svg`, label: 'B&Q Nano G2 Ultra' },
|
||||
{ match: /t114/i, image: `${IMG}/heltec-mesh-node-t114.svg`, label: 'Heltec Mesh Node T114' },
|
||||
{ match: /wireless\s*stick|wsl/i, image: `${IMG}/heltec-wsl-v3.svg`, label: 'Heltec Wireless Stick Lite V3' },
|
||||
{ match: /heltec.*v4/i, image: `${IMG}/heltec_v4.svg`, label: 'Heltec V4' },
|
||||
{ match: /heltec/i, image: `${IMG}/heltec-v3.svg`, label: 'Heltec LoRa 32 V3' },
|
||||
{ match: /thinknode/i, image: `${IMG}/thinknode_m1.svg`, label: 'Elecrow ThinkNode' },
|
||||
{ match: /pico/i, image: `${IMG}/rpipicow.svg`, label: 'Raspberry Pi Pico' },
|
||||
{ match: /rnode/i, image: `${IMG}/diy.svg`, label: 'RNode' },
|
||||
const PRODUCT_MATCHES: Array<{ match: RegExp; model: string; label: string }> = [
|
||||
{ match: /t-?deck/i, model: 't-deck', label: 'LILYGO T-Deck' },
|
||||
{ match: /t-?echo\s*plus/i, model: 't-echo_plus', label: 'LILYGO T-Echo Plus' },
|
||||
{ match: /t-?echo/i, model: 't-echo', label: 'LILYGO T-Echo' },
|
||||
{ match: /t-?beam\s*s3/i, model: 'tbeam-s3-core', label: 'LILYGO T-Beam S3' },
|
||||
{ match: /t-?beam/i, model: 'tbeam', label: 'LILYGO T-Beam' },
|
||||
{ match: /t3.?s3/i, model: 'tlora-t3s3-v1', label: 'LILYGO T3-S3' },
|
||||
{ match: /t-?lora|tlora/i, model: 'tlora-v2-1-1_6', label: 'LILYGO T-LoRa' },
|
||||
{ match: /rak.?4631|wisblock/i, model: 'rak4631', label: 'RAK WisBlock 4631' },
|
||||
{ match: /wismesh|wistap/i, model: 'rak-wismeshtap', label: 'RAK WisMesh Tap' },
|
||||
{ match: /t1000|tracker.?t1000/i, model: 'tracker-t1000-e', label: 'Seeed Card Tracker T1000-E' },
|
||||
{ match: /xiao/i, model: 'seeed-xiao-s3', label: 'Seeed XIAO S3' },
|
||||
{ match: /wio.?tracker|wm1110/i, model: 'wio-tracker-wm1110', label: 'Seeed Wio Tracker' },
|
||||
{ match: /station\s*g2/i, model: 'station-g2', label: 'B&Q Station G2' },
|
||||
{ match: /nano\s*g2/i, model: 'nano-g2-ultra', label: 'B&Q Nano G2 Ultra' },
|
||||
{ match: /t114/i, model: 'heltec-mesh-node-t114', label: 'Heltec Mesh Node T114' },
|
||||
{ match: /wireless\s*stick|wsl/i, model: 'heltec-wsl-v3', label: 'Heltec Wireless Stick Lite V3' },
|
||||
{ match: /heltec.*v4/i, model: 'heltec_v4', label: 'Heltec V4' },
|
||||
{ match: /heltec/i, model: 'heltec-v3', label: 'Heltec LoRa 32 V3' },
|
||||
{ match: /thinknode/i, model: 'thinknode_m1', label: 'Elecrow ThinkNode' },
|
||||
{ match: /pico/i, model: 'rpipicow', label: 'Raspberry Pi Pico' },
|
||||
{ match: /rnode/i, model: 'diy', label: 'RNode' },
|
||||
]
|
||||
|
||||
/** vid:pid → most-likely board (bridge chips = chip-level guess) */
|
||||
const VIDPID_MATCHES: Record<string, { image: string; label: string; exact: boolean }> = {
|
||||
const VIDPID_MATCHES: Record<string, { model: string; label: string; exact: boolean }> = {
|
||||
// Silicon Labs CP210x — Heltec V3 family ships this bridge
|
||||
'10c4:ea60': { image: `${IMG}/heltec-v3.svg`, label: 'LoRa radio (CP2102 serial)', exact: false },
|
||||
'10c4:ea60': { model: 'heltec-v3', label: 'LoRa radio (CP2102 serial)', exact: false },
|
||||
// WCH CH340 — most T-Beam / T-LoRa boards
|
||||
'1a86:7523': { image: `${IMG}/tbeam.svg`, label: 'LoRa radio (CH340 serial)', exact: false },
|
||||
'1a86:55d4': { image: `${IMG}/tbeam.svg`, label: 'LoRa radio (CH9102 serial)', exact: false },
|
||||
'1a86:7523': { model: 'tbeam', label: 'LoRa radio (CH340 serial)', exact: false },
|
||||
'1a86:55d4': { model: 'tbeam', label: 'LoRa radio (CH9102 serial)', exact: false },
|
||||
// Espressif native USB (S3 boards: T3-S3, T-Deck without product str)
|
||||
'303a:1001': { image: `${IMG}/tlora-t3s3-v1.svg`, label: 'ESP32-S3 LoRa board', exact: false },
|
||||
'303a:1001': { model: 'tlora-t3s3-v1', label: 'ESP32-S3 LoRa board', exact: false },
|
||||
// RAK4631 nRF52 native USB
|
||||
'239a:8029': { image: `${IMG}/rak4631.svg`, label: 'RAK WisBlock 4631', exact: true },
|
||||
'239a:8029': { model: 'rak4631', label: 'RAK WisBlock 4631', exact: true },
|
||||
// Raspberry Pi (Pico W)
|
||||
'2e8a:0005': { image: `${IMG}/rpipicow.svg`, label: 'Raspberry Pi Pico', exact: false },
|
||||
'2e8a:0005': { model: 'rpipicow', label: 'Raspberry Pi Pico', exact: false },
|
||||
}
|
||||
|
||||
const FALLBACK: MeshDeviceImage = {
|
||||
image: `${IMG}/unknown-new.svg`,
|
||||
model: 'unknown-new',
|
||||
label: 'LoRa mesh radio',
|
||||
exact: false,
|
||||
}
|
||||
@@ -79,7 +76,7 @@ export function resolveMeshDeviceImage(info: DetectedDeviceInfo | undefined): Me
|
||||
const product = `${info.manufacturer ?? ''} ${info.product ?? ''}`.trim()
|
||||
if (product) {
|
||||
for (const m of PRODUCT_MATCHES) {
|
||||
if (m.match.test(product)) return { image: m.image, label: m.label, exact: true }
|
||||
if (m.match.test(product)) return { model: m.model, label: m.label, exact: true }
|
||||
}
|
||||
}
|
||||
if (info.vid && info.pid) {
|
||||
|
||||
@@ -280,13 +280,16 @@ function stepIconUrl(step: GoalStep): string | undefined {
|
||||
* generic services list. `launchAppId` opens via the app launcher (iframe apps
|
||||
* overlay on top of the current screen; X-Frame-Options apps open a tab).
|
||||
*/
|
||||
const GOAL_COMPLETION_CTA: Record<string, { label: string; route?: string; launchAppId?: string }> = {
|
||||
const GOAL_COMPLETION_CTA: Record<string, { label: string; route?: string | { path: string; query?: Record<string, string> }; launchAppId?: string }> = {
|
||||
'instant-business-site': { label: 'Open Just Works', launchAppId: 'justworks' },
|
||||
'open-a-shop': { label: 'Go to my shop (BTCPay)', launchAppId: 'btcpay-server' },
|
||||
'accept-payments': { label: 'Go to Lightning (LND)', route: '/dashboard/apps/lnd' },
|
||||
'run-lightning-node': { label: 'View my channels', route: '/dashboard/apps/lnd/channels' },
|
||||
'setup-fedimint': { label: 'Open Fedimint', launchAppId: 'fedimint' },
|
||||
'file-browser': { label: 'Open File Browser', launchAppId: 'filebrowser' },
|
||||
// Keep the completion action inside Archipelago's native Cloud browser. The
|
||||
// old CTA launched the standalone File Browser app, which lost the Cloud
|
||||
// navigation context and bypassed the original in-app file browser view.
|
||||
'file-browser': { label: 'Open my cloud files', route: { path: '/dashboard/cloud', query: { tab: 'mine' } } },
|
||||
'store-files': { label: 'Open my cloud (Nextcloud)', launchAppId: 'nextcloud' },
|
||||
'create-identity': { label: 'Go to my identity', route: '/dashboard/web5' },
|
||||
'back-up-everything': { label: 'Go to backups', route: '/dashboard/settings' },
|
||||
|
||||
@@ -163,13 +163,22 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<router-link
|
||||
to="/dashboard/server/firewall-tunnels"
|
||||
class="glass-button mt-4 w-full gap-2 rounded-lg text-sm font-medium"
|
||||
>
|
||||
Firewalls & tunnels
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
|
||||
</router-link>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-4">
|
||||
<router-link
|
||||
to="/dashboard/server/firewall-tunnels"
|
||||
class="glass-button w-full gap-2 rounded-lg text-sm font-medium"
|
||||
>
|
||||
Firewalls & tunnels
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/dashboard/server/openwrt"
|
||||
class="glass-button w-full gap-2 rounded-lg text-sm font-medium"
|
||||
>
|
||||
OpenWrt Gateway
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FipsNetworkCard />
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import BackButton from '@/components/BackButton.vue'
|
||||
import DeviceModel from '@/components/DeviceModel.vue'
|
||||
import HardwareMarketplace from '@/components/HardwareMarketplace.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const shopOpen = ref(false)
|
||||
const showcase = ref<HTMLElement>()
|
||||
const showcaseHeight = ref<string>()
|
||||
|
||||
function openShop() {
|
||||
const height = showcase.value?.getBoundingClientRect().height
|
||||
if (height) showcaseHeight.value = `${height}px`
|
||||
shopOpen.value = true
|
||||
}
|
||||
async function closeShop() {
|
||||
shopOpen.value = false
|
||||
await nextTick()
|
||||
showcaseHeight.value = undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pb-12">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<BackButton desktop-margin="mb-0" @click="router.push({ path: '/dashboard', query: { tab: 'setup' } })" />
|
||||
<h1 class="text-lg font-semibold text-white">Get a non surveillance phone</h1>
|
||||
</div>
|
||||
<p class="text-sm text-white/75 max-w-2xl">Your phone should answer to you. Choose a supported Pixel, install GrapheneOS, and keep verified boot on.</p>
|
||||
|
||||
<div ref="showcase" class="glass-card p-6 overflow-hidden phone-showcase" :style="showcaseHeight ? { height: showcaseHeight } : undefined">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_minmax(280px,360px)] gap-6 items-start phone-showcase__grid">
|
||||
<DeviceModel v-if="!shopOpen" model="pixel-graphene" label="Black Pixel 9 Pro with the GrapheneOS boot screen" fill-viewport :controls="false" />
|
||||
<HardwareMarketplace kind="phone" :expanded="shopOpen" @open="openShop" @close="closeShop" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.phone-showcase { margin: 24px 0 48px; }
|
||||
.phone-showcase__grid { height: 100%; min-height: 0; grid-template-rows: minmax(0, 1fr); }
|
||||
@media (min-width: 1024px) {
|
||||
.phone-showcase__grid > :deep(.router-marketplace) { align-self: end; margin-top: 0; }
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useCachedResource } from '@/composables/useCachedResource'
|
||||
import BackButton from '@/components/BackButton.vue'
|
||||
import DeviceModel from '@/components/DeviceModel.vue'
|
||||
import HardwareMarketplace from '@/components/HardwareMarketplace.vue'
|
||||
|
||||
const router = useRouter()
|
||||
function goBack() {
|
||||
@@ -113,6 +115,19 @@ const host = ref('')
|
||||
const sshUser = ref('root')
|
||||
const sshPassword = ref('')
|
||||
const showConnectForm = ref(false)
|
||||
const routerShopOpen = ref(false)
|
||||
const routerCard = ref<HTMLElement>()
|
||||
const routerCardHeight = ref<string>()
|
||||
function openRouterShop() {
|
||||
const height = routerCard.value?.getBoundingClientRect().height
|
||||
if (height) routerCardHeight.value = `${height}px`
|
||||
routerShopOpen.value = true
|
||||
}
|
||||
async function closeRouterShop() {
|
||||
routerShopOpen.value = false
|
||||
await nextTick()
|
||||
routerCardHeight.value = undefined
|
||||
}
|
||||
const connecting = ref(false)
|
||||
const connectedParams = ref<Record<string, string> | null>(null)
|
||||
|
||||
@@ -456,7 +471,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pb-6">
|
||||
<div class="pb-12">
|
||||
<!-- mb-0 overrides the pill's default mb-4, which pushed it above the
|
||||
title's centerline in this inline header row. -->
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
@@ -465,7 +480,8 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Connect form -->
|
||||
<div v-if="showConnectForm" class="glass-card p-6 mb-6">
|
||||
<template v-if="showConnectForm">
|
||||
<div class="glass-card p-6 mb-6">
|
||||
<h2 class="text-sm font-semibold text-white/80 mb-4">Connect to Router</h2>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
@@ -530,6 +546,16 @@ onMounted(() => {
|
||||
<p v-if="error" class="mt-3 text-xs text-red-400">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<div ref="routerCard" class="glass-card p-6 mb-6 overflow-hidden router-model-card" :style="routerCardHeight ? { height: routerCardHeight } : undefined">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_minmax(280px,360px)] gap-6 items-start">
|
||||
<DeviceModel v-if="!routerShopOpen" model="beryl-ax" label="Beryl AX (GL-MT3000) OpenWrt router" fill-viewport :controls="false" />
|
||||
<HardwareMarketplace :expanded="routerShopOpen" @open="openRouterShop" @close="closeRouterShop" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-16" aria-hidden="true"></div>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- Loading skeleton -->
|
||||
<template v-else-if="loading">
|
||||
<div v-for="i in 3" :key="i" class="glass-card p-6 mb-4 animate-pulse">
|
||||
@@ -1004,5 +1030,20 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.router-model-card {
|
||||
margin: 24px 0 48px;
|
||||
}
|
||||
.router-model-card > .grid { height: 100%; min-height: 0; grid-template-rows: minmax(0, 1fr); }
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.router-model-card > .grid > :deep(.router-marketplace) {
|
||||
align-self: end;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||