diff --git a/BUILD_GUIDE.md b/BUILD_GUIDE.md
index 2a863a4..fc05abd 100644
--- a/BUILD_GUIDE.md
+++ b/BUILD_GUIDE.md
@@ -193,6 +193,14 @@ Extracted from the v3.0 netlist (identical functions to v2.x — the same firmwa
## 6. Case Assembly
+### ▶️ Video: from printed parts to first power-on
+
+Sections 6, 7, and 9 are covered in **one YouTube video** — the full assembly, wiring, and first boot. Click to watch:
+
+[](https://youtu.be/J9C9bZgkNKs)
+
+**[▶ Watch on YouTube — Patternflow v3.0 assembly & first power-on](https://youtu.be/J9C9bZgkNKs)**
+
By now the enclosure halves you bonded in Section 4 have cured and the board is soldered — time to bring them together:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3852572..ea2f9c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,7 @@ All notable changes to Patternflow will be documented in this file.
### Added
- **v3.0 board promoted to the recommended revision** (`hardware/pcb/gerber/patternflow_v3.0_gerber.zip`). Fabricated, assembled, and verified ([#114](https://github.com/engmung/Patternflow/issues/114)): hybrid power input — USB-C (`USB1` + 5.1k CC pull-downs) or a back-side 2-pin screw terminal (`J4`) as the beginner bypass — all-through-hole, no SMD passives. **Not size-compatible with v2.x cases (and vice versa).**
- **Machine-readable BOM** (`hardware/bom/bom_v3.0.csv`): every part by manufacturer part number, per-unit quantities, assembly-critical notes. The LED panel remains an AliExpress link (its mounting-screw positions match the case).
-- **v3 build guide** — now the main `BUILD_GUIDE.md`: soldering covered by a full [video walkthrough](https://youtu.be/NZCjMBCsDAc), photo-documented print/case/wiring steps, netlist-derived pin reference. The v2 guide moved to `BUILD_GUIDE_v2.md` for existing v2.x builds.
+- **v3 build guide** — now the main `BUILD_GUIDE.md`: [PCB soldering](https://youtu.be/NZCjMBCsDAc) and [assembly-to-first-power-on](https://youtu.be/J9C9bZgkNKs) each covered by a full video walkthrough, photo-documented print/case/wiring steps, netlist-derived pin reference. The v2 guide moved to `BUILD_GUIDE_v2.md` for existing v2.x builds.
### Changed
- **`hardware/case/` reorganized by printer bed size**: `bed_256mm/encloser.stl` (the standard print — body frame, back panels, and LED-panel mount in one STL, ~10 h) and `bed_330mm/` (one-piece snap-fit); knobs print separately from `knobs/` in black, required for every build. `bed_256mm/for_other_panels/` holds the community divided variant with an adjustable LED-panel mount ([#169](https://github.com/engmung/Patternflow/issues/169)-tested) for panels other than the BOM-linked one. All v2.x-board cases moved to `case/legacy_v2/`. The snap-fit design adds two wall-mount holes and recesses for the panel's alignment bumps — no more nipper trimming ([#19](https://github.com/engmung/Patternflow/issues/19)).
diff --git a/docs/releases/v3.0.0-launch.md b/docs/releases/v3.0.0-launch.md
index c8ba1ea..bb832b9 100644
--- a/docs/releases/v3.0.0-launch.md
+++ b/docs/releases/v3.0.0-launch.md
@@ -4,10 +4,7 @@ Everything for the v3.0.0 release is **pre-loaded**. When the final gate below p
## Gate 0 — the one thing left (hardware)
-Print `hardware/case/bed_256mm/encloser.stl` on the v3 unit, assemble with a v3.0 board, and confirm:
-
-- [ ] USB-C / J4 cutouts and board bay line up (this exact STL has never been printed — only its verified v2.1 twin)
-- [ ] Run the full checklist in [BUILD_GUIDE.md §9](../../BUILD_GUIDE.md#9-final-checks)
+- [x] **PASSED (2026-07-19).** The v3 `encloser.stl` printed, assembled with a v3.0 board, and works — documented end to end in the [assembly video](https://youtu.be/J9C9bZgkNKs), §9 checks included.
## Gate 1 — swap the two v2.1-twin photos
diff --git a/docs/releases/v3.0.0.md b/docs/releases/v3.0.0.md
index 993294f..b020429 100644
--- a/docs/releases/v3.0.0.md
+++ b/docs/releases/v3.0.0.md
@@ -20,7 +20,7 @@ The biggest hardware revision since launch. The v3.0 board and enclosure were re
- [`hardware/bom/bom_v3.0.csv`](https://github.com/engmung/Patternflow/blob/v3.0.0/hardware/bom/bom_v3.0.csv) — machine-readable, every part specified by MPN, sourcing notes included. Order from Mouser/DigiKey/LCSC or find the same parts anywhere.
### The guide: watch, don't decipher
-- [BUILD_GUIDE.md](https://github.com/engmung/Patternflow/blob/v3.0.0/BUILD_GUIDE.md) rewritten for v3.0 — soldering as one YouTube video, photo-documented printing/bonding/assembly, a collapsible netlist-derived pin reference, and a dedicated OSC/Ableton build path.
+- [BUILD_GUIDE.md](https://github.com/engmung/Patternflow/blob/v3.0.0/BUILD_GUIDE.md) rewritten for v3.0 — **two full video walkthroughs** ([PCB soldering](https://youtu.be/NZCjMBCsDAc) and [assembly to first power-on](https://youtu.be/J9C9bZgkNKs)), photo-documented printing/bonding steps, a collapsible netlist-derived pin reference, and a dedicated OSC/Ableton build path.
- PCB ordering routed through the [PCBWay shared project](https://www.pcbway.com/project/shareproject/Patternflow_An_LED_synthesizer_776d796c.html) (no Gerber upload needed) — any fab works with the attached zip too.
### Firmware
diff --git a/hardware/case/README.md b/hardware/case/README.md
index 9a9f74d..c140b4a 100644
--- a/hardware/case/README.md
+++ b/hardware/case/README.md
@@ -24,7 +24,7 @@ The original mass-production-oriented design: a single-piece body plus a snap-fi
## `bed_256mm/` — the standard build
-**`encloser.stl`** puts the whole body in one file: frame and back-panel halves plus the LED-panel mounting part, all in **white** PLA. ~10 hours total on a P1S. Print the knobs separately from [`knobs/`](knobs/), in **black**. The LED-panel mount is sized for the panel linked in the [BOM](../bom/). (Its v2.1 twin — same design, cut for the v2.1 board — is print & assembly verified; this file is the v3.0 cut.)
+**`encloser.stl`** puts the whole body in one file: frame and back-panel halves plus the LED-panel mounting part, all in **white** PLA. ~10 hours total on a P1S. Print the knobs separately from [`knobs/`](knobs/), in **black**. The LED-panel mount is sized for the panel linked in the [BOM](../bom/). **Print & assembly verified** — the [assembly video](https://youtu.be/J9C9bZgkNKs) builds from this exact file.
Design perks: **two wall-mount holes**, a **snap-fit back panel**, and recesses for the LED matrix's alignment bumps — **no more nipper-trimming** the panel back (the old [#19](https://github.com/engmung/Patternflow/issues/19) workaround).
diff --git a/web/ARCHITECTURE.md b/web/ARCHITECTURE.md
index c6e8e29..f1e789b 100644
--- a/web/ARCHITECTURE.md
+++ b/web/ARCHITECTURE.md
@@ -15,7 +15,6 @@ The `web/` app is the Patternflow site at [patternflow.work](https://patternflow
| `/roadmap` | Roadmap rendered from GitHub issues via `/api/roadmap` |
| `/api/roadmap` | Server route that pulls open issues + sub-issue progress (10 min revalidate; optional `GITHUB_TOKEN` for rate limit) |
| `/pattern-lab` | **Internal, noindex.** Pattern authoring/curation workspace: Monaco editor, preset library, BYOK Gemini generation, JS→C++ conversion prompt |
-| `/video-baker` | **Internal, noindex.** Bakes video clips into 128×64 PFV loops for the hardware |
| `/business` · `/contact` | Static pages |
| `feed.xml` · `sitemap.ts` · `robots.ts` | Feeds and SEO plumbing |
@@ -47,8 +46,6 @@ This is the core of the site and mirrors the firmware:
- **`src/lib/patternHarness.ts`** — runs pattern JS in the browser on a 128×64 virtual matrix with 4 virtual encoders (20 detents/turn), matching device semantics (`knobDeltas`, `btnPressed`/`btnHeld`).
- **`src/lib/gemini.ts`** — bring-your-own-key Gemini generation for Pattern Lab. The key lives in `localStorage` and calls go straight from the browser to Google; no server proxy, no bundled key.
- **Flasher** — esp-web-tools driven by `public/flash/manifest.json` + prebuilt binaries in `public/flash/bin/`.
-- **`src/lib/serialUpload.ts`** — Web Serial upload of PFV data to a running device (`PFV::\n` + raw bytes).
-- **`src/lib/pfv.ts`** — PFV1 binary LED video format (128×64, RGB565). **`src/lib/ledPipeline.ts`** — fit/crop/dither correction pipeline. **`src/lib/videoDecoder.ts`** — mp4box + WebCodecs decode for the Video Baker.
## Content pipeline
diff --git a/web/README.md b/web/README.md
index 4e3179b..fb81b0c 100644
--- a/web/README.md
+++ b/web/README.md
@@ -32,7 +32,7 @@ None are required — the site runs fully without them.
## Where things live
-- `src/app/` — routes (App Router). `/pattern-lab` and `/video-baker` are internal noindex tools.
+- `src/app/` — routes (App Router). `/pattern-lab` is an internal noindex tool.
- `src/components/` — 3D viewer, landing sections, journal renderer.
- `src/lib/presets/` — JS pattern library; **source of truth** for the firmware preset headers.
- `content/` — markdown/MDX site copy and journal articles. Editing copy means editing these files.
diff --git a/web/package-lock.json b/web/package-lock.json
index 156676a..47128ff 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -22,7 +22,6 @@
"feed": "^5.2.1",
"gray-matter": "^4.0.3",
"leva": "^0.10.1",
- "mp4box": "^2.3.0",
"next": "16.2.4",
"posthog-js": "^1.371.4",
"react": "19.2.4",
@@ -8323,15 +8322,6 @@
"@types/trusted-types": "^2.0.7"
}
},
- "node_modules/mp4box": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/mp4box/-/mp4box-2.3.0.tgz",
- "integrity": "sha512-nnABYbdh4UguEYyV+uRwQBi1tbb8kXka2Fx9yKzmDKAeh8gkvRKYxoK1XDd8GQIjSfN4rvsXrW1CBo4yRQJZDA==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=20.8.1"
- }
- },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
diff --git a/web/package.json b/web/package.json
index 7f65de2..f11193d 100644
--- a/web/package.json
+++ b/web/package.json
@@ -23,7 +23,6 @@
"feed": "^5.2.1",
"gray-matter": "^4.0.3",
"leva": "^0.10.1",
- "mp4box": "^2.3.0",
"next": "16.2.4",
"posthog-js": "^1.371.4",
"react": "19.2.4",
diff --git a/web/src/app/pattern-lab/PatternLab.module.css b/web/src/app/pattern-lab/PatternLab.module.css
index 991f290..69560d7 100644
--- a/web/src/app/pattern-lab/PatternLab.module.css
+++ b/web/src/app/pattern-lab/PatternLab.module.css
@@ -313,24 +313,6 @@
accent-color: #171512;
}
-.rampDemo {
- margin-left: auto;
- min-height: 28px;
- border: 1px solid rgba(23, 21, 18, 0.22);
- background: #ffffff;
- color: #171512;
- padding: 0 8px;
- font: inherit;
- font-family: var(--font-jetbrains), monospace;
- font-size: 11px;
- text-transform: uppercase;
- cursor: pointer;
-}
-
-.rampDemo:hover {
- border-color: #171512;
-}
-
.rampTrack {
position: relative;
height: 34px;
@@ -654,6 +636,19 @@
gap: 10px;
}
+.presetStep {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.presetStep span {
+ font-family: var(--font-jetbrains), monospace;
+ font-size: 12px;
+ min-width: 44px;
+ text-align: center;
+}
+
.editorHeader .guideButton {
background: #e8dcc2 !important;
color: #171512 !important;
diff --git a/web/src/app/pattern-lab/PatternLabClient.tsx b/web/src/app/pattern-lab/PatternLabClient.tsx
index 3c667f9..b81055c 100644
--- a/web/src/app/pattern-lab/PatternLabClient.tsx
+++ b/web/src/app/pattern-lab/PatternLabClient.tsx
@@ -52,10 +52,15 @@ import {
} from "@/lib/patternPatch";
import { captureEvent } from "@/lib/posthogEvents";
import SharePatternModal from "@/components/share/SharePatternModal";
-import { preset as originPreset } from "@/lib/presets/pattern-origin";
import { livePresets } from "@/lib/presets";
import styles from "./PatternLab.module.css";
+// Pattern Lab's own presets — the labOnly set (built on lab features like the
+// color ramp / value field) that the /pattern showcase hides. The header
+// stepper browses these; the editor opens on the first one.
+const labPresets = livePresets.filter((preset) => preset.labOnly);
+const initialLabPreset = labPresets[0] ?? livePresets[0];
+
const DEFAULT_KNOB_LABELS = ["Knob 1", "Knob 2", "Knob 3", "Knob 4"];
const initialKnobs = [...LOGICAL_KNOB_DEFAULTS];
@@ -89,6 +94,22 @@ function parseKnobsAnnotation(code: string): KnobAnnotationEntry[] | null {
return result.some(Boolean) ? result : null;
}
const defaultRanges: KnobRange[] = LOGICAL_KNOB_RANGES.map(([min, max]) => [min, max]);
+
+// The initial preset seeds useState directly (bypassing updateCode), so honor
+// its @knobs annotation here; every later load goes through applyKnobsAnnotation.
+const initialAnnotationRaw = initialLabPreset.code.match(KNOBS_ANNOTATION_RE)?.[0] ?? null;
+const initialAnnotation = initialAnnotationRaw ? parseKnobsAnnotation(initialLabPreset.code) : null;
+const presetKnobLabels = DEFAULT_KNOB_LABELS.map(
+ (label, index) => initialAnnotation?.[index]?.name ?? label,
+);
+const presetRanges: KnobRange[] = defaultRanges.map((range, index) => {
+ const entry = initialAnnotation?.[index];
+ return entry ? [entry.min, entry.max] : range;
+});
+const presetKnobs = initialKnobs.map((value, index) => {
+ const entry = initialAnnotation?.[index];
+ return entry ? Math.max(entry.min, Math.min(entry.max, value)) : value;
+});
const sweepValues = [0, 0.25, 0.5, 0.75, 1];
const minRangeSpan = 0.001;
const pixelsPerDigitStep = 10;
@@ -256,50 +277,6 @@ function loadStoredPatch(): PatchState {
}
}
-// Demo pattern for the value-field workflow: pure 0..1 field via setValue,
-// color comes entirely from the Color Ramp panel.
-const VFIELD_DEMO_CODE = `// V-field demo — this pattern outputs only a 0..1 value field.
-// Color comes from the Color Ramp panel, not from this code.
-// @knobs Warp=0..2.2, Speed=0.1..10, Zoom=0.6..3, Bands=0..8
-
-export function setup(params) {
- params.t = 0;
-}
-
-export function update(dt, input, params) {
- const kv = input.knobValues || [1.1, 2.0, 1.5, 0];
- params.warp = kv[0];
- params.speed = kv[1];
- params.zoom = kv[2];
- params.bands = Math.round(kv[3]);
- params.t += dt * params.speed * 0.3;
-}
-
-export function draw(display, params, time) {
- const w = display.width;
- const h = display.height;
- const t = params.t;
- const zoom = params.zoom;
- const cx = 0.5 + 0.22 * Math.sin(t * 0.7);
- const cy = 0.5 + 0.22 * Math.cos(t * 0.9);
-
- for (let y = 0; y < h; y++) {
- const ny = (y / h - 0.5);
- for (let x = 0; x < w; x++) {
- const nx = (x / h - w / h * 0.5);
- const dx = x / h - cx * (w / h);
- const dy = y / h - cy;
- const ring = Math.sin((dx * dx + dy * dy) * 14 * zoom - t * 2.0);
- const wave = Math.sin(nx * 6 * zoom + t + params.warp * Math.sin(ny * 5 * zoom - t * 0.8));
- let v = 0.5 + 0.25 * ring + 0.25 * wave;
- if (params.bands > 1) {
- v = Math.floor(v * params.bands) / (params.bands - 1);
- }
- display.setValue(x, y, v);
- }
- }
-}`;
-
type GalleryItem = PatternVariant & { id: string; pinned?: boolean };
// Cap the gallery without ever dropping pinned (kept) items.
@@ -483,10 +460,10 @@ function VariantPreview({
}
export default function PatternLabClient() {
- const [code, setCode] = useState(originPreset.code);
- const [knobs, setKnobs] = useState(initialKnobs);
- const [ranges, setRanges] = useState(defaultRanges);
- const [knobLabels, setKnobLabels] = useState(DEFAULT_KNOB_LABELS);
+ const [code, setCode] = useState(initialLabPreset.code);
+ const [knobs, setKnobs] = useState(presetKnobs);
+ const [ranges, setRanges] = useState(presetRanges);
+ const [knobLabels, setKnobLabels] = useState(presetKnobLabels);
const [running, setRunning] = useState(true);
const [runtimeError, setRuntimeError] = useState(null);
const [renderStats, setRenderStats] = useState({ fps: 0, ms: 0 });
@@ -522,7 +499,7 @@ export default function PatternLabClient() {
const canvasRef = useRef(null);
const runtimeRef = useRef(null);
const knobsRef = useRef(knobs);
- const previousKnobsRef = useRef(initialKnobs);
+ const previousKnobsRef = useRef(presetKnobs);
const runningRef = useRef(running);
const simTimeRef = useRef(0);
const runtimeErrorRef = useRef(null);
@@ -796,7 +773,7 @@ export default function PatternLabClient() {
// Apply a pattern's @knobs annotation only when the annotation text itself
// changes — manual label/range edits persist until different code arrives.
- const lastKnobsAnnotationRef = useRef(null);
+ const lastKnobsAnnotationRef = useRef(initialAnnotationRaw);
const applyKnobsAnnotation = (nextCode: string) => {
const raw = nextCode.match(KNOBS_ANNOTATION_RE)?.[0] ?? null;
@@ -821,12 +798,24 @@ export default function PatternLabClient() {
};
// Single entry point for replacing the editor code so annotations apply on
- // every path (typing/pasting, gallery load, demo, patch send).
+ // every path (typing/pasting, gallery load, preset step, patch send).
const updateCode = (nextCode: string) => {
applyKnobsAnnotation(nextCode);
setCode(nextCode);
};
+ // ── Lab preset stepper ── browses the labOnly set; index follows the editor
+ // content, so editing away from a preset shows "–".
+ const activeLabIndex = labPresets.findIndex((preset) => preset.code === code);
+
+ const stepLabPreset = (dir: number) => {
+ if (labPresets.length === 0) return;
+ const base = activeLabIndex >= 0 ? activeLabIndex : dir > 0 ? -1 : 0;
+ const next = (base + dir + labPresets.length) % labPresets.length;
+ updateCode(labPresets[next].code);
+ setEditorView("code");
+ };
+
useEffect(() => {
knobsRef.current = knobs;
}, [knobs]);
@@ -1625,14 +1614,6 @@ ${activeCode}
/>
recolor
-
) : (
<>
+