diff --git a/docs/editor.md b/docs/editor.md index dc1c7db..16a7908 100644 --- a/docs/editor.md +++ b/docs/editor.md @@ -8,6 +8,8 @@ The Apps collection uses the same installable-app selection rules as `/apps`. `p ## About page simulator +On wide screens, the working badge sits beside the hardware specifications. Narrow screens put the badge first. `src/lib/badge-controls.ts` supplies the approved control names and SVG icons to both the About page buttons and `badge-control-symbols.astro`. The shared 3D view draws these same SVGs beside the five physical buttons and the existing Home touchpad; clicking a symbol uses its existing input bit. Both model hosts include the SVG source component, so the editor's 3D model gets the same markings without changing its separate page controls. No case-model rebuild is required. + `/about-badge/` boots the Mona OS 4.0 menu in the same worker and renders it on the editor's 3D badge model (`initBadgeModel`). Zooming moves the orbit target toward the badge screen as the camera closes in, and back to the model centre when zoomed out. `npm run sync:editor-apps` bundles the menu from `badger/home` `badge/apps/menu` as the catalog `launcher`. Each device `.mpy` module is replaced by its `src/.py` source, because the simulator cannot load `.mpy` files. `launcher.stock` lists the stock apps, taken from the menu's `anim/.bin` icons. The page stages only the menu at `/system/apps/menu` and the stock apps at `/system/apps/`. `src/lib/badge-firmware-compat.ts` registers stand-ins for firmware-frozen modules that are missing from the vendored runtime (`aye_arr` and `badgeware.ir`) in `sys.modules`. `public/simulator/filesystem/sparkle.py` replaces the firmware's Sparkle supervisor: `request_local_show()` wraps the next `run` call to draw the local show's bouncing Invertocat and case lights, with the Bluetooth mesh shimmed out. Three SELECT presses or the 10-minute timeout end the show and call `machine.reset()`. It also patches newer firmware APIs: `display.set_async`, `machine.reset_cause`, and the three-argument `missing_details.show`. Both this page and editor simulations use it; no extra files are staged. IR does nothing in the simulator. A Python launcher in `src/lib/badge-os-simulator.ts` replicates `badge/main.py`: it runs the menu, launches the selected app, and returns to the menu when HOME is pressed or the app calls `reset()` or fails. This loop is needed because `machine.reset()` and pin IRQs are inert in the simulator. The page shows the badge photo until the 3D model has loaded and the first frame renders. If the browser lacks module workers, WebAssembly, WebGL, or `ImageData`, or the simulator or model errors or times out, the worker stops and the photo remains. The Examples collection includes all 36 examples from the simulator's categorized manifest at revision `eb3b3fbf27e6e61635aef5de11ab17e6303e8c20`. `public/editor-examples/` includes their original source, manifest categories and descriptions, required images and fonts, and MIT notice. Run `npm run sync:editor-examples` to restore them from that pinned revision. Update `simulatorRevision` in `scripts/editor-example-source.mjs` only when updating the bundled runtime and its provenance notices together. Opening an example creates an editable `example-` app with an `__init__.py` entry point. References to bundled `/system/assets/` files become app-local asset paths, so first installs include those dependencies and avoid collisions with existing badge app names. diff --git a/src/components/badge-control-symbols.astro b/src/components/badge-control-symbols.astro new file mode 100644 index 0000000..6cd75b4 --- /dev/null +++ b/src/components/badge-control-symbols.astro @@ -0,0 +1,9 @@ +--- +import { badgeControls } from '@/lib/badge-controls' +--- + + diff --git a/src/components/badge-os-simulator.astro b/src/components/badge-os-simulator.astro index 2e2abc1..0c86e76 100644 --- a/src/components/badge-os-simulator.astro +++ b/src/components/badge-os-simulator.astro @@ -1,17 +1,11 @@ --- -import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, CornerDownLeft, House, Maximize, Minus, Plus, RotateCw, ScanSearch } from 'lucide-react' +import { Maximize, Minus, Plus, RotateCw, ScanSearch } from 'lucide-react' +import BadgeControlSymbols from '@/components/badge-control-symbols.astro' import BadgeImage from '@/components/badge-image.astro' +import { badgeControls } from '@/lib/badge-controls' -const badgeButtons = [ - { icon: ArrowLeft, name: 'Left', bit: 16 }, - { icon: ArrowRight, name: 'Right', bit: 8 }, - { icon: CornerDownLeft, name: 'Enter', bit: 4 }, -] -const sideButtons = [ - { icon: ArrowUp, name: 'Up', bit: 2 }, - { icon: ArrowDown, name: 'Down', bit: 1 }, - { icon: House, name: 'Home', bit: 32 }, -] +const badgeButtons = badgeControls.slice(0, 3) +const sideButtons = badgeControls.slice(3) ---
@@ -32,6 +26,7 @@ const sideButtons = [
+
@@ -80,7 +75,7 @@ const sideButtons = [ .badge-os-stack[data-state='fallback'] [data-fallback-caption] { display: block; } .badge-os-model :global(canvas:not([data-screen])) { display: block; width: 100%; height: 100%; touch-action: none; outline: none; } .badge-os-model :global(canvas:not([data-screen]):focus-visible) { outline: 2px solid hsl(var(--primary)); outline-offset: -2px; } - .badge-os-zoom { position: absolute; right: 0.5rem; bottom: 0.5rem; display: flex; flex-direction: column; gap: 0.375rem; } + .badge-os-zoom { position: absolute; right: 0.5rem; top: 0.5rem; display: flex; flex-direction: column; gap: 0.375rem; } .badge-os-key { width: 2.75rem; height: 2.25rem; border: 1px solid hsl(var(--border) / 0.4); border-radius: 0.375rem; background: transparent; font-size: 0.8rem; touch-action: none; user-select: none; transition: border-color 150ms, background-color 150ms; } .badge-os-key:hover:not(:disabled) { border-color: hsl(var(--primary) / 0.5); background: hsl(var(--primary) / 0.05); } .badge-os-key:disabled { opacity: 0.4; } diff --git a/src/components/doom-simulator.astro b/src/components/doom-simulator.astro index 0ba032f..0988b46 100644 --- a/src/components/doom-simulator.astro +++ b/src/components/doom-simulator.astro @@ -4,6 +4,7 @@ import DoomBadgeLinks from './doom-badge-links.astro' import DoomVideo from './doom-video.astro' import DoomMobileControls from './doom-mobile-controls.astro' import DoomControls from './doom-controls.astro' +import BadgeControlSymbols from './badge-control-symbols.astro' import { sitePath } from '@/lib/site-path' --- @@ -29,6 +30,7 @@ import { sitePath } from '@/lib/site-path'
+ GitHub badge with DOOM running on its screen
diff --git a/src/lib/badge-controls.ts b/src/lib/badge-controls.ts new file mode 100644 index 0000000..b1f6d3f --- /dev/null +++ b/src/lib/badge-controls.ts @@ -0,0 +1,10 @@ +import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp, CornerDownLeft, House } from 'lucide-react' + +export const badgeControls = [ + { icon: ChevronLeft, name: 'Left', bit: 16 }, + { icon: ChevronRight, name: 'Right', bit: 8 }, + { icon: CornerDownLeft, name: 'Select', bit: 4 }, + { icon: ChevronUp, name: 'Up', bit: 2 }, + { icon: ChevronDown, name: 'Down', bit: 1 }, + { icon: House, name: 'Home', bit: 32 }, +] as const diff --git a/src/lib/badge-editor-model.ts b/src/lib/badge-editor-model.ts index e149589..b23e762 100644 --- a/src/lib/badge-editor-model.ts +++ b/src/lib/badge-editor-model.ts @@ -1,6 +1,7 @@ -import { Box3, CanvasTexture, DirectionalLight, DoubleSide, HemisphereLight, LinearFilter, Mesh, MeshBasicMaterial, MeshStandardMaterial, NearestFilter, PerspectiveCamera, Raycaster, Scene, SRGBColorSpace, Vector2, Vector3, WebGLRenderer, type Material, type Object3D } from 'three' +import { Box3, CanvasTexture, DirectionalLight, DoubleSide, Group, HemisphereLight, LinearFilter, Mesh, MeshBasicMaterial, MeshStandardMaterial, NearestFilter, PerspectiveCamera, Raycaster, Scene, SRGBColorSpace, Vector2, Vector3, WebGLRenderer, type Material, type Object3D } from 'three' import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js' import { OrbitControls } from 'three/addons/controls/OrbitControls.js' +import { SVGLoader } from 'three/addons/loaders/SVGLoader.js' import { displayOverlayMatrix } from './badge-display-overlay' import { initBadgeIdleRotation } from './badge-model-idle-rotation' import { sitePath } from './site-path' @@ -374,6 +375,7 @@ export function initBadgeModel(host: HTMLElement, screen: HTMLCanvasElement, but }) if (!displayMesh || caps.size !== 5) throw new Error('The badge model is missing its screen or controls.') roundPanels(model) + addControlSymbols(model) if (disposed) { disposeModel(); return } updateFrame() setButtons(mask) @@ -446,6 +448,56 @@ export function initBadgeModel(host: HTMLElement, screen: HTMLCanvasElement, but } } + function addControlSymbols(root: Object3D) { + const front = root.getObjectByName('Front_electronic_assembly_face') + const sources = host.querySelectorAll('[data-badge-symbol]') + if (!front || !artwork || sources.length !== 6) throw new Error('The badge model is missing its control symbols.') + const frontBounds = new Box3().setFromObject(front) + const artBounds = new Box3().setFromObject(artwork.mesh) + const home = touchPads.find(pad => pad.bit === 32)! + const loader = new SVGLoader() + // White is the model's printed ink, not a page-theme color. + const ink = new MeshBasicMaterial({ color: 0xffffff, side: DoubleSide, toneMapped: false }) + for (const source of sources) { + const bit = Number(source.dataset.badgeSymbol) + const cap = caps.get(bit) + let center: Vector3 + let size = 0.006 + if (cap) { + center = new Box3().setFromObject(cap.mesh).getCenter(new Vector3()) + if (bit === 2) center.y += 0.0065 + else if (bit === 1) center.y -= 0.0065 + else center.x += 0.0065 + center.z = frontBounds.max.z + 0.00005 + } else if (bit === 32) { + size = 0.005 + center = new Vector3( + artBounds.min.x + (home.x0 + home.x1) / 2 / padTexture.width * (artBounds.max.x - artBounds.min.x), + artBounds.max.y - (home.y0 + home.y1) / 2 / padTexture.height * (artBounds.max.y - artBounds.min.y) - 0.0055, + artBounds.max.z + 0.00005, + ) + } else continue + const svg = source.cloneNode(true) as SVGSVGElement + svg.setAttribute('stroke', '#fff') + const group = new Group() + group.name = `Control_symbol_${source.dataset.badgeSymbolName}` + root.worldToLocal(center) + group.position.set(center.x - size / 2, center.y + size / 2, center.z) + group.scale.set(size / 24, -size / 24, size / 24) + root.add(group) + for (const path of loader.parse(new XMLSerializer().serializeToString(svg)).paths) { + for (const subpath of path.subPaths) { + const geometry = SVGLoader.pointsToStroke(subpath.getPoints(16), path.userData!.style) + if (!geometry) continue + const stroke = new Mesh(geometry, ink) + group.add(stroke) + targets.set(stroke, bit) + } + } + if (!group.children.length) throw new Error(`Could not draw the badge ${source.dataset.badgeSymbolName} symbol.`) + } + } + function disposeModel() { const materials = new Set(discarded) const textures = new Set([texture]) diff --git a/src/pages/about-badge.astro b/src/pages/about-badge.astro index ed61152..cff5076 100644 --- a/src/pages/about-badge.astro +++ b/src/pages/about-badge.astro @@ -75,14 +75,19 @@ const specifications = [

About

About the Badger

-

Inspect the 2026 Badger 2350 hardware, ports, controls, and sensors, then use MonaOS in the browser.

+

Inspect the 2026 Badger 2350 hardware, ports, controls, and sensors, then use MonaOS in the browser.

-
-
-

Hardware & specs

-
+
+
+

Badge simulator

+ +
+ +
+

Hardware & specs

+
{specifications.map(({ title, icon: Icon, details, uses }) => ( @@ -110,13 +115,6 @@ const specifications = [ ))}
-
-
- -
-
-

Badge simulator

- -
-
+ +
diff --git a/src/pages/simulator.astro b/src/pages/simulator.astro index 95bccba..817e5de 100644 --- a/src/pages/simulator.astro +++ b/src/pages/simulator.astro @@ -1,5 +1,6 @@ --- import Layout from '@/layouts/Layout.astro' +import BadgeControlSymbols from '@/components/badge-control-symbols.astro' import { Code2, Play, Square, Upload, Download, FolderOpen, Terminal, Cpu, Usb, RefreshCw, ArrowRight, Undo2, Redo2, Grid2X2, Maximize, Minimize, X, Search, Trash2, Plus, RotateCw } from 'lucide-react' --- @@ -69,6 +70,7 @@ import { Code2, Play, Square, Upload, Download, FolderOpen, Terminal, Cpu, Usb,
+
Your code starts here.

Loading 3D view… You can use the simulator now.