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:
+3
-1
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user