Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<module>.py` source, because the simulator cannot load `.mpy` files. `launcher.stock` lists the stock apps, taken from the menu's `anim/<app>.bin` icons. The page stages only the menu at `/system/apps/menu` and the stock apps at `/system/apps/<name>`. `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-<name>` 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.

Expand Down
9 changes: 9 additions & 0 deletions src/components/badge-control-symbols.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
import { badgeControls } from '@/lib/badge-controls'
---

<div hidden aria-hidden="true" data-badge-symbols>
{badgeControls.map(({ icon: Icon, name, bit }) => (
<Icon data-badge-symbol={bit} data-badge-symbol-name={name} aria-hidden="true" />
))}
</div>
19 changes: 7 additions & 12 deletions src/components/badge-os-simulator.astro
Original file line number Diff line number Diff line change
@@ -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)
---

<div id="badge-os-simulator" data-state="loading" class="badge-os-stack">
Expand All @@ -32,6 +26,7 @@ const sideButtons = [
<BadgeImage />
</div>
<div data-model class="badge-os-model">
<BadgeControlSymbols />
<canvas data-screen width="320" height="240" hidden></canvas>
<p data-model-status hidden></p>
<div class="badge-os-zoom" role="group" aria-label="Zoom">
Expand Down Expand Up @@ -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; }
Expand Down
2 changes: 2 additions & 0 deletions src/components/doom-simulator.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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'
---

Expand All @@ -29,6 +30,7 @@ import { sitePath } from '@/lib/site-path'

<div class="doom-stage">
<div data-doom-model data-state="idle" class="doom-model">
<BadgeControlSymbols />
<img data-doom-preview src={sitePath('/images/doom-badge-preview.webp')} alt="GitHub badge with DOOM running on its screen" width="293" height="600" fetchpriority="high" class="doom-preview" />
<canvas data-doom-runtime-screen width="320" height="200" aria-hidden="true"></canvas>
<div data-doom-fallback class="doom-fallback">
Expand Down
10 changes: 10 additions & 0 deletions src/lib/badge-controls.ts
Original file line number Diff line number Diff line change
@@ -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
54 changes: 53 additions & 1 deletion src/lib/badge-editor-model.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<SVGSVGElement>('[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])
Expand Down
26 changes: 12 additions & 14 deletions src/pages/about-badge.astro
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,19 @@ const specifications = [
<div class="container">
<p class="font-mono text-xs uppercase tracking-[0.16em] text-primary">About</p>
<h1 class="mt-4 text-5xl leading-[0.98] sm:text-6xl">About the Badger</h1>
<p class="mt-6 max-w-prose text-muted-foreground">Inspect the 2026 Badger 2350 hardware, ports, controls, and sensors, then use MonaOS in the browser.</p>
<p class="mt-6 text-muted-foreground">Inspect the 2026 Badger 2350 hardware, ports, controls, and sensors, then use MonaOS in the browser.</p>
</div>
</section>

<section id="specifications" class="scroll-mt-28 pb-12 pt-8 sm:pb-16 sm:pt-10">
<div class="container">
<h2 class="mb-8 text-3xl sm:text-4xl">Hardware &amp; specs</h2>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div class="container grid items-start gap-8 pb-12 pt-8 sm:pb-16 sm:pt-10 lg:grid-cols-2">
<section id="badge-simulator" class="min-w-0 scroll-mt-28">
<h2 class="mb-6 text-3xl sm:text-4xl">Badge simulator</h2>
<BadgeOsSimulator />
</section>

<section id="specifications" class="min-w-0 scroll-mt-28">
<h2 class="mb-6 text-3xl sm:text-4xl">Hardware &amp; specs</h2>
<div class="grid gap-4 sm:grid-cols-2">
{specifications.map(({ title, icon: Icon, details, uses }) => (
<Card className="flex h-full flex-col">
<CardHeader>
Expand Down Expand Up @@ -110,13 +115,6 @@ const specifications = [
</Card>
))}
</div>
</div>
</section>

<section id="badge-simulator" class="scroll-mt-28 border-t border-border/70 bg-muted/25 py-12 sm:py-16">
<div class="container">
<h2 class="mb-6 text-3xl sm:text-4xl">Badge simulator</h2>
<BadgeOsSimulator />
</div>
</section>
</section>
</div>
</Layout>
2 changes: 2 additions & 0 deletions src/pages/simulator.astro
Original file line number Diff line number Diff line change
@@ -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'
---

Expand Down Expand Up @@ -69,6 +70,7 @@ import { Code2, Play, Square, Upload, Download, FolderOpen, Terminal, Cpu, Usb,
</div>
</div>
<div id="badge-model" class="badge-model" data-state="loading">
<BadgeControlSymbols />
<canvas id="badge-screen" width="320" height="240" tabindex="0" aria-label="Simulated badge display. Arrow keys control the badge, Space selects. WASD and J control the gamepad."></canvas>
<div id="screen-placeholder" class="pointer-events-none absolute inset-0 grid place-content-center gap-2 text-center"><Code2 size={28} className="mx-auto text-primary" /><span class="text-xs text-[#8b949e]">Your code starts here.</span></div>
<p id="model-status" role="status" class="pointer-events-none absolute inset-x-4 bottom-3 text-center text-xs text-muted-foreground">Loading 3D view… You can use the simulator now.</p>
Expand Down
Loading