From 2a432eb84204dbe59e9cb7dd3890c56dae7edf99 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Fri, 2 Oct 2026 00:03:30 +0100 Subject: [PATCH] web/home: add the pi, snes and floppy machines Signed-off-by: oppiliappan --- .../electronics/FloppyComputer.stories.svelte | 16 + .../home/electronics/FloppyComputer.svelte | 257 +++++++++++++++++ .../electronics/PiCartridge.stories.svelte | 16 + .../home/electronics/PiCartridge.svelte | 183 ++++++++++++ .../electronics/SnesCartridge.stories.svelte | 16 + .../home/electronics/SnesCartridge.svelte | 176 +++++++++++ .../components/home/electronics/pi.test.ts | 25 ++ web/src/lib/components/home/electronics/pi.ts | 273 ++++++++++++++++++ 8 files changed, 962 insertions(+) create mode 100644 web/src/lib/components/home/electronics/FloppyComputer.stories.svelte create mode 100644 web/src/lib/components/home/electronics/FloppyComputer.svelte create mode 100644 web/src/lib/components/home/electronics/PiCartridge.stories.svelte create mode 100644 web/src/lib/components/home/electronics/PiCartridge.svelte create mode 100644 web/src/lib/components/home/electronics/SnesCartridge.stories.svelte create mode 100644 web/src/lib/components/home/electronics/SnesCartridge.svelte create mode 100644 web/src/lib/components/home/electronics/pi.test.ts create mode 100644 web/src/lib/components/home/electronics/pi.ts diff --git a/web/src/lib/components/home/electronics/FloppyComputer.stories.svelte b/web/src/lib/components/home/electronics/FloppyComputer.stories.svelte new file mode 100644 index 000000000..cc332632f --- /dev/null +++ b/web/src/lib/components/home/electronics/FloppyComputer.stories.svelte @@ -0,0 +1,16 @@ + + + +
+ +
+
diff --git a/web/src/lib/components/home/electronics/FloppyComputer.svelte b/web/src/lib/components/home/electronics/FloppyComputer.svelte new file mode 100644 index 000000000..79a9704e8 --- /dev/null +++ b/web/src/lib/components/home/electronics/FloppyComputer.svelte @@ -0,0 +1,257 @@ + + + + + + + + + + + + {#each vents as d, i (i)} + + {/each} + + {#each drives as drive, i (i)} + + + + + {/each} + + + + {#each panelVents as d, i (i)} + + {/each} + + + + + + + + + + {#each keys as shape, i (i)} + + {/each} + + + + {#each disk.guides as [from, to], i (i)} + + {/each} + + + + + + {#each disk.holes as d, i (i)} + + {/each} + + + + diff --git a/web/src/lib/components/home/electronics/PiCartridge.stories.svelte b/web/src/lib/components/home/electronics/PiCartridge.stories.svelte new file mode 100644 index 000000000..088a9aa09 --- /dev/null +++ b/web/src/lib/components/home/electronics/PiCartridge.stories.svelte @@ -0,0 +1,16 @@ + + + +
+ +
+
diff --git a/web/src/lib/components/home/electronics/PiCartridge.svelte b/web/src/lib/components/home/electronics/PiCartridge.svelte new file mode 100644 index 000000000..73015990a --- /dev/null +++ b/web/src/lib/components/home/electronics/PiCartridge.svelte @@ -0,0 +1,183 @@ + + +{#snippet rod(part: Part)} + {@const [[ax, ay], [bx, by]] = axisOf(part, part.z0, part.z1)} + + +{/snippet} + +{#snippet guide(part: Part)} + {@const [from, to] = axisOf(part, part.z1, part.z0 + 1)} + {#if from[1] < to[1] - 10} + + {/if} +{/snippet} + +{#snippet component(part: Part)} + + + {#each part.marks ?? [] as m, i (i)} + + {/each} + + +{/snippet} + +{#snippet shell(part: Part)} + + + {#each part.marks ?? [] as m, i (i)} + + {/each} + {#each ridgesOf(part) as d (d)} + + {/each} + + + +{/snippet} + + + + + + + {#each holes as d, i (i)} + + {/each} + + + {#each ordered as part (part.id)} + {#if part.kind === "rod"} + {@render rod(part)} + {:else if part.kind === "guide"} + {@render guide(part)} + {:else if part.id === "cartridge"} + {@render shell(part)} + {:else} + {@render component(part)} + {/if} + {/each} + diff --git a/web/src/lib/components/home/electronics/SnesCartridge.stories.svelte b/web/src/lib/components/home/electronics/SnesCartridge.stories.svelte new file mode 100644 index 000000000..3941bfdad --- /dev/null +++ b/web/src/lib/components/home/electronics/SnesCartridge.stories.svelte @@ -0,0 +1,16 @@ + + + +
+ +
+
diff --git a/web/src/lib/components/home/electronics/SnesCartridge.svelte b/web/src/lib/components/home/electronics/SnesCartridge.svelte new file mode 100644 index 000000000..3bb9ce45e --- /dev/null +++ b/web/src/lib/components/home/electronics/SnesCartridge.svelte @@ -0,0 +1,176 @@ + + + + + + + + {#each grooves as d, i (i)} + + {/each} + + {#each ports as d, i (i)} + + {/each} + {#each pins as d, i (i)} + + {/each} + + + + + + {#each cart.guides as [from, to], i (i)} + + {/each} + + + + + + + + + + {#each switches as shape, i (i)} + + {/each} + + diff --git a/web/src/lib/components/home/electronics/pi.test.ts b/web/src/lib/components/home/electronics/pi.test.ts new file mode 100644 index 000000000..5a2def8a4 --- /dev/null +++ b/web/src/lib/components/home/electronics/pi.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; +import { paintOrder } from "$lib/components/home/electronics/iso"; +import { board, cartridge } from "$lib/components/home/electronics/pi"; + +describe("board", () => { + it("paints every part once", () => { + const parts = [...board(), ...cartridge(20)]; + expect(new Set(paintOrder(parts).map((p) => p.id)).size).toBe(parts.length); + }); +}); + +describe("cartridge", () => { + it("seats its socket on the header's base, clear of the pins' tops", () => { + const header = board().find((p) => p.id === "header")!; + const socket = cartridge(0).find((p) => p.id === "socket")!; + expect(socket.z0).toBe(header.z1); + expect(socket.x0).toBeLessThanOrEqual(header.x0); + expect(socket.y1).toBeGreaterThanOrEqual(header.y1); + }); + + it("only shows its guides while it is off the board", () => { + expect(cartridge(0).some((p) => p.kind === "guide")).toBe(false); + expect(cartridge(30).filter((p) => p.kind === "guide")).toHaveLength(2); + }); +}); diff --git a/web/src/lib/components/home/electronics/pi.ts b/web/src/lib/components/home/electronics/pi.ts new file mode 100644 index 000000000..14f6668ea --- /dev/null +++ b/web/src/lib/components/home/electronics/pi.ts @@ -0,0 +1,273 @@ +import { + arched, + circle, + cuboid, + rect, + span, + stadium, + type Box, + type Face, + type Pt, + type Pt3 +} from "$lib/components/home/electronics/iso"; + +export interface Mark { + face: Face; + points: Pt[]; + recess?: boolean; +} + +export interface Part extends Box { + id: string; + fine?: boolean; + kind?: "box" | "rod" | "guide"; + marks?: Mark[]; +} + +const LENGTH = 85; +const T = 1.6; + +export const BOARD = { ...cuboid([0, 0, 0], [56, LENGTH, T]), r: 3 }; + +const turnPoint = ([u, v]: Pt): Pt => [v, LENGTH - u]; + +const turn = (p: Part): Part => ({ + ...p, + x0: p.y0, + x1: p.y1, + y0: LENGTH - p.x1, + y1: LENGTH - p.x0, + marks: p.marks?.flatMap((mark): Mark[] => { + if (mark.face === "top") return [{ ...mark, points: mark.points.map(turnPoint) }]; + if (mark.face === "front") + return [ + { ...mark, face: "side", points: mark.points.map(([u, v]) => [LENGTH - u, v]) } + ]; + return []; + }) +}); + +export const HOLES: Pt[] = ( + [ + [3.5, 3.5], + [61.5, 3.5], + [3.5, 52.5], + [61.5, 52.5] + ] as Pt[] +).map(turnPoint); + +const box = (id: string, min: Pt3, max: Pt3, extra: Partial = {}): Part => ({ + id, + ...cuboid(min, max), + ...extra +}); + +const pin = (id: string, x: number, y: number, z0: number, z1: number) => + box(id, [x - 0.32, y - 0.32, z0], [x + 0.32, y + 0.32, z1], { fine: true, kind: "rod" }); + +const HEADER = { cx: 32.5, cy: 3.5, pitch: 2.54, base: 2.5, pin: 6 }; +const HEADER_TOP = T + HEADER.base; +const PINS_TOP = HEADER_TOP + HEADER.pin; + +const header = (): Part[] => { + const { cx, cy, pitch } = HEADER; + return [ + box("header", [cx - 10 * pitch, cy - pitch, T], [cx + 10 * pitch, cy + pitch, HEADER_TOP]), + ...[0, 1].flatMap((row) => + Array.from({ length: 20 }, (_, i) => + pin( + `pin-${row}-${i}`, + cx + (i - 9.5) * pitch, + cy + (row - 0.5) * pitch, + HEADER_TOP, + PINS_TOP + ) + ) + ) + ]; +}; + +const usb = (id: string, y0: number): Part => { + const y1 = y0 + 13.1; + const tab = (c: number): Mark => ({ + face: "top", + points: [ + [86, c - 2.2], + [81.4, c], + [86, c + 2.2], + [86, c + 1.2], + [83.4, c], + [86, c - 1.2] + ] + }); + return box(id, [67, y0, T], [87.1, y1, T + 16], { marks: [tab(y0 + 3.6), tab(y1 - 3.6)] }); +}; + +const hdmi = (id: string, x0: number): Part => { + const x1 = x0 + 7.5; + return box(id, [x0, 49, T], [x1, 57.3, T + 3.5], { + marks: [ + { + face: "front", + points: [ + [x0 + 0.6, 2.9], + [x1 - 0.6, 2.9], + [x1 - 0.6, 1.6], + [x1 - 1.5, 0.6], + [x0 + 1.5, 0.6], + [x0 + 0.6, 1.6] + ] + } + ] + }); +}; + +const mipi = (id: string, x0: number): Part => + box(id, [x0, 39, T], [x0 + 3, 52, T + 1.2], { + marks: [{ face: "top", points: rect(span(x0 + 1.9, 39.4, x0 + 2.7, 51.6)), recess: true }] + }); + +const PASSIVES: [number, number, boolean][] = [ + [23, 22, false], + [23, 25, false], + [23, 28, false], + [23, 31, false], + [23, 34, false], + [43.4, 22, false], + [43.4, 25, false], + [43.4, 34, false], + [28.5, 38.6, true], + [32.5, 38.6, true], + [36.5, 38.6, true], + [28.5, 18.4, true], + [32.5, 18.4, true], + [36.5, 18.4, true], + [55.6, 24.5, false], + [55.6, 27.5, false], + [12.5, 38.4, true], + [15.5, 38.4, true], + [18.5, 38.4, true], + [13.5, 47.8, true], + [48.5, 33.5, true], + [51.5, 33.5, true] +]; + +const passive = ([x, y, across]: [number, number, boolean], k: number) => { + const [du, dv] = across ? [0.25, 0.5] : [0.5, 0.25]; + return box(`passive-${k}`, [x - du, y - dv, T], [x + du, y + dv, T + 0.4], { fine: true }); +}; + +const onTop = (points: Pt[], recess = false): Mark[] => [{ face: "top", points, recess }]; + +const flat = (): Part[] => [ + ...header(), + box("fan", [59, 7, T], [63.2, 10.2, T + 2.9], { + marks: onTop(rect(span(59.6, 7.9, 62.6, 9.4))) + }), + box("wireless", [5, 9, T], [16.5, 20, T + 1.6], { marks: onTop(circle([7, 11], 0.5, 10)) }), + box("ram", [8.5, 24, T], [20.5, 36, T + 1], { marks: onTop(circle([9.6, 25.1], 0.45, 10)) }), + box("soc", [25, 20, T], [42, 37, T + 0.9]), + box("soc-lid", [27, 22, T + 0.9], [40, 35, T + 2.3]), + box("rp1", [46, 22.5, T], [54, 30.5, T + 1], { marks: onTop(circle([47, 23.5], 0.4, 10)) }), + box("pmic", [5, 41, T], [11, 46.5, T + 0.9]), + box("inductor", [12.5, 41.5, T], [15, 44, T + 1.2]), + ...PASSIVES.map(passive), + box("pcie", [0.6, 20, T], [4.4, 36, T + 1.3], { + marks: onTop(rect(span(0.9, 20.4, 1.8, 35.6)), true) + }), + box("button", [-0.4, 40, T], [2.8, 43.6, T + 2.6], { + marks: onTop(circle([1.2, 41.8], 1, 16)) + }), + box("led-power", [0.9, 45, T], [2, 46, T + 0.6], { fine: true }), + box("led-activity", [0.9, 47.2, T], [2, 48.2, T + 0.6], { fine: true }), + box("rtc", [17, 44.5, T], [20.5, 47.5, T + 2], { + marks: onTop(rect(span(17.6, 45.3, 19.9, 46.6))) + }), + box("usb-c", [7, 49.5, T], [16, 57.3, T + 3.2], { + marks: [ + { face: "front", points: stadium(span(7.8, 0.5, 15.2, 2.7)) }, + { face: "front", points: rect(span(9.6, 1.4, 13.4, 1.8)) } + ] + }), + hdmi("hdmi-0", 22), + box("uart", [31.2, 51, T], [34.4, 54.2, T + 2.8], { + marks: onTop(rect(span(31.8, 51.8, 33.8, 53.4))) + }), + hdmi("hdmi-1", 36), + mipi("mipi-0", 45), + mipi("mipi-1", 50.5), + box("poe", [57.4, 45.2, T], [62.5, 50.3, T + 2.5]), + ...[0, 1].flatMap((row) => + [0, 1].map((col) => + pin(`poe-pin-${row}-${col}`, 58.68 + col * 2.54, 46.48 + row * 2.54, T + 2.5, T + 7) + ) + ), + usb("usb-2", 2.5), + usb("usb-3", 20.5), + box("ethernet", [65.5, 38, T], [87.1, 54, T + 13.5], { + marks: onTop(rect(span(82.5, 39, 86.3, 53))) + }) +]; + +export const board = (): Part[] => flat().map(turn); + +const SEAT = HEADER_TOP; +const SOCKET = 8.5; +const SHELL = { thick: 8, length: 57, tall: 52 }; + +const RECESS = { inset: 4.5, bottom: 5, top: 39 }; +export const LOGO = { size: 20, v: (RECESS.bottom + RECESS.top) / 2 }; + +export const RIDGES = { at: [42.5, 44.6, 46.7, 48.8], depth: 0.9, reach: 9.5 }; + +export const cartridge = (lift: number): Part[] => { + const { cx, cy, pitch } = HEADER; + const z = SEAT + lift; + const base = z + SOCKET; + const x = cy; + const y = LENGTH - cx; + const along = 10 * pitch; + const guides = + lift > 1 + ? [-1, 1].map((side) => + box( + `guide-${side}`, + [x - 0.1, y + side * along - 0.1, HEADER_TOP], + [x + 0.1, y + side * along + 0.1, z], + { kind: "guide" } + ) + ) + : []; + const y0 = y - SHELL.length / 2; + const y1 = y + SHELL.length / 2; + return [ + ...guides, + box( + "socket", + [x - pitch - 0.3, y - along - 0.2, z], + [x + pitch + 0.3, y + along + 0.2, base] + ), + box( + "cartridge", + [x - SHELL.thick / 2, y0, base], + [x + SHELL.thick / 2, y1, base + SHELL.tall], + { + marks: [ + { + face: "side", + points: arched( + span(y0 + RECESS.inset, RECESS.bottom, y1 - RECESS.inset, RECESS.top), + 3 + ), + recess: true + }, + { + face: "side", + points: stadium(span(y0 + 14, 43.5, y1 - 14, 49.5)), + recess: true + } + ] + } + ) + ]; +}; -- 2.51.2