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
8 changes: 8 additions & 0 deletions BUILD_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

[![▶ YouTube — Patternflow v3.0 enclosure assembly & first power-on](https://img.youtube.com/vi/J9C9bZgkNKs/maxresdefault.jpg)](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:

<img src="docs/build-guide/images/v3/11_ready_to_mate.jpg" width="70%">
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand Down
5 changes: 1 addition & 4 deletions docs/releases/v3.0.0-launch.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/releases/v3.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion hardware/case/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
3 changes: 0 additions & 3 deletions web/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down Expand Up @@ -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:<name>:<size>\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

Expand Down
2 changes: 1 addition & 1 deletion web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 0 additions & 10 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 13 additions & 18 deletions web/src/app/pattern-lab/PatternLab.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
115 changes: 55 additions & 60 deletions web/src/app/pattern-lab/PatternLabClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<KnobRange[]>(defaultRanges);
const [knobLabels, setKnobLabels] = useState<string[]>(DEFAULT_KNOB_LABELS);
const [code, setCode] = useState(initialLabPreset.code);
const [knobs, setKnobs] = useState(presetKnobs);
const [ranges, setRanges] = useState<KnobRange[]>(presetRanges);
const [knobLabels, setKnobLabels] = useState<string[]>(presetKnobLabels);
const [running, setRunning] = useState(true);
const [runtimeError, setRuntimeError] = useState<string | null>(null);
const [renderStats, setRenderStats] = useState({ fps: 0, ms: 0 });
Expand Down Expand Up @@ -522,7 +499,7 @@ export default function PatternLabClient() {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const runtimeRef = useRef<PatternRuntime | null>(null);
const knobsRef = useRef(knobs);
const previousKnobsRef = useRef(initialKnobs);
const previousKnobsRef = useRef(presetKnobs);
const runningRef = useRef(running);
const simTimeRef = useRef(0);
const runtimeErrorRef = useRef<string | null>(null);
Expand Down Expand Up @@ -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<string | null>(null);
const lastKnobsAnnotationRef = useRef<string | null>(initialAnnotationRaw);

const applyKnobsAnnotation = (nextCode: string) => {
const raw = nextCode.match(KNOBS_ANNOTATION_RE)?.[0] ?? null;
Expand All @@ -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]);
Expand Down Expand Up @@ -1625,14 +1614,6 @@ ${activeCode}
/>
recolor
</label>
<button
type="button"
className={styles.rampDemo}
title="Load a demo pattern that draws a 0..1 value field via display.setValue"
onClick={() => updateCode(VFIELD_DEMO_CODE)}
>
V demo
</button>
</div>
<div
ref={rampTrackRef}
Expand Down Expand Up @@ -1868,6 +1849,20 @@ ${activeCode}
</>
) : (
<>
<div
className={styles.presetStep}
title={activeLabIndex >= 0 ? labPresets[activeLabIndex].name : "Lab presets"}
>
<button type="button" onClick={() => stepLabPreset(-1)} aria-label="Previous lab preset">
</button>
<span>
{activeLabIndex >= 0 ? activeLabIndex + 1 : "–"}/{labPresets.length}
</span>
<button type="button" onClick={() => stepLabPreset(1)} aria-label="Next lab preset">
</button>
</div>
<button type="button" onClick={copyVariantPrompt}>
{promptCopied ? "Copied" : "Copy prompt"}
</button>
Expand Down
2 changes: 1 addition & 1 deletion web/src/app/robots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function robots(): MetadataRoute.Robots {
// Search engines and AI crawlers are welcome on public pages.
userAgent: "*",
allow: "/",
disallow: ["/api/", "/pattern-lab", "/video-baker"],
disallow: ["/api/", "/pattern-lab"],
},
],
sitemap: `${siteUrl}/sitemap.xml`,
Expand Down
Loading
Loading