From d5134b8ba5e6d3bfdbcea177876e3c5cb63e140a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Thu, 9 Jul 2026 11:29:37 +0200 Subject: [PATCH] feat: add pcb world with instanced components Implements Task 6: seeded PRNG for deterministic PCB layout, substrate, copper trace road, and instanced chip/cap/resistor/via components. All tests pass. Build successful. --- src/App.tsx | 4 +- src/scene/World.tsx | 85 +++++++++++++++++++++++++++++++++++++++++++ src/scene/rng.test.ts | 13 +++++++ src/scene/rng.ts | 11 ++++++ 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 src/scene/World.tsx create mode 100644 src/scene/rng.test.ts create mode 100644 src/scene/rng.ts diff --git a/src/App.tsx b/src/App.tsx index 24e703f..2c1a7d3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { Canvas } from '@react-three/fiber' import { CameraRig } from './scene/CameraRig' +import { World } from './scene/World' import { useRideInput } from './ride/useRideInput' export default function App() { @@ -15,7 +16,8 @@ export default function App() { - {/* World (Task 6), Bus (Task 7), Sky (Task 8), + + {/* Bus (Task 7), Sky (Task 8), Passengers (Task 7), Stops (Task 9) mount here */} diff --git a/src/scene/World.tsx b/src/scene/World.tsx new file mode 100644 index 0000000..913e4c2 --- /dev/null +++ b/src/scene/World.tsx @@ -0,0 +1,85 @@ +import { useMemo } from 'react' +import { Instance, Instances } from '@react-three/drei' +import { TubeGeometry, Vector3 } from 'three' +import { busPath, pointAt, tangentAt } from './path' +import { mulberry32 } from './rng' + +interface Item { + pos: [number, number, number] + rot: number + scale: [number, number, number] +} + +function layout(seed: number, count: number, minOff: number, maxOff: number): Item[] { + const rng = mulberry32(seed) + const items: Item[] = [] + for (let i = 0; i < count; i++) { + const t = rng() + const side = rng() > 0.5 ? 1 : -1 + const p = pointAt(t) + const tan = tangentAt(t) + const off = minOff + rng() * (maxOff - minOff) + const n = new Vector3(-tan.z, 0, tan.x).multiplyScalar(side * off) + const s = 0.8 + rng() * 2.4 + items.push({ + pos: [p.x + n.x, s / 2, p.z + n.z], + rot: rng() * Math.PI, + scale: [s * (1 + rng()), s, s], + }) + } + return items +} + +export function World() { + const chips = useMemo(() => layout(1337, 40, 9, 40), []) + const caps = useMemo(() => layout(4242, 50, 8, 38), []) + const resistors = useMemo(() => layout(90210, 50, 7, 36), []) + const vias = useMemo(() => layout(555, 80, 6, 44), []) + const trace = useMemo(() => new TubeGeometry(busPath, 200, 0.5, 6, false), []) + + return ( + + {/* substrate */} + + + + + {/* main copper trace = the road */} + + + + {/* chips (black QFP buildings) */} + + + + {chips.map((it, i) => ( + + ))} + + {/* electrolytic caps (cylinders) */} + + + + {caps.map((it, i) => ( + + ))} + + {/* resistors (tan boxes) */} + + + + {resistors.map((it, i) => ( + + ))} + + {/* vias (gold pads) */} + + + + {vias.map((it, i) => ( + + ))} + + + ) +} diff --git a/src/scene/rng.test.ts b/src/scene/rng.test.ts new file mode 100644 index 0000000..2cfa8c3 --- /dev/null +++ b/src/scene/rng.test.ts @@ -0,0 +1,13 @@ +import { expect, it } from 'vitest' +import { mulberry32 } from './rng' + +it('is deterministic and in [0,1)', () => { + const a = mulberry32(1337) + const b = mulberry32(1337) + for (let i = 0; i < 100; i++) { + const v = a() + expect(v).toBe(b()) + expect(v).toBeGreaterThanOrEqual(0) + expect(v).toBeLessThan(1) + } +}) diff --git a/src/scene/rng.ts b/src/scene/rng.ts new file mode 100644 index 0000000..75b721a --- /dev/null +++ b/src/scene/rng.ts @@ -0,0 +1,11 @@ +/** Deterministic PRNG so the PCB layout is stable across builds. */ +export function mulberry32(seed: number): () => number { + let a = seed >>> 0 + return () => { + a += 0x6d2b79f5 + let z = a + z = Math.imul(z ^ (z >>> 15), z | 1) + z ^= z + Math.imul(z ^ (z >>> 7), z | 61) + return ((z ^ (z >>> 14)) >>> 0) / 4294967296 + } +}