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
+ }
+}