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.
This commit is contained in:
L'électron rare
2026-07-09 11:29:37 +02:00
parent cc133d5734
commit d5134b8ba5
4 changed files with 112 additions and 1 deletions
+3 -1
View File
@@ -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() {
<ambientLight intensity={0.55} />
<directionalLight position={[60, 90, 30]} intensity={1.3} />
<CameraRig />
{/* World (Task 6), Bus (Task 7), Sky (Task 8),
<World />
{/* Bus (Task 7), Sky (Task 8),
Passengers (Task 7), Stops (Task 9) mount here */}
</Canvas>
</div>
+85
View File
@@ -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 (
<group>
{/* substrate */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[100, -0.05, 0]}>
<planeGeometry args={[520, 260]} />
<meshStandardMaterial color="#0a3d1e" roughness={0.9} />
</mesh>
{/* main copper trace = the road */}
<mesh geometry={trace} position={[0, 0.02, 0]}>
<meshStandardMaterial color="#c47a2c" metalness={0.7} roughness={0.35} emissive="#552f00" />
</mesh>
{/* chips (black QFP buildings) */}
<Instances limit={chips.length}>
<boxGeometry />
<meshStandardMaterial color="#15151c" roughness={0.6} />
{chips.map((it, i) => (
<Instance key={i} position={it.pos} rotation={[0, it.rot, 0]} scale={it.scale} />
))}
</Instances>
{/* electrolytic caps (cylinders) */}
<Instances limit={caps.length}>
<cylinderGeometry args={[0.6, 0.6, 1, 10]} />
<meshStandardMaterial color="#2b3a67" metalness={0.5} roughness={0.4} />
{caps.map((it, i) => (
<Instance key={i} position={it.pos} scale={[it.scale[1], it.scale[1] * 1.6, it.scale[1]]} />
))}
</Instances>
{/* resistors (tan boxes) */}
<Instances limit={resistors.length}>
<boxGeometry args={[2, 0.7, 0.7]} />
<meshStandardMaterial color="#c8a165" roughness={0.8} />
{resistors.map((it, i) => (
<Instance key={i} position={[it.pos[0], 0.35, it.pos[2]]} rotation={[0, it.rot, 0]} scale={it.scale[1]} />
))}
</Instances>
{/* vias (gold pads) */}
<Instances limit={vias.length}>
<cylinderGeometry args={[0.5, 0.5, 0.1, 8]} />
<meshStandardMaterial color="#e0b341" metalness={0.9} roughness={0.2} emissive="#4a3500" />
{vias.map((it, i) => (
<Instance key={i} position={[it.pos[0], 0.05, it.pos[2]]} />
))}
</Instances>
</group>
)
}
+13
View File
@@ -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)
}
})
+11
View File
@@ -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
}
}