feat: replace primitives with kenney cc0 models
Swap hand-built boxes/cylinders for Kenney Car Kit, City Kit Roads, and Furniture Kit low-poly models across the bus, stops, and world, keeping ride/camera logic and Html panels untouched.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+22
-31
@@ -1,13 +1,28 @@
|
||||
import { useRef } from 'react'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import { Clone, useGLTF } from '@react-three/drei'
|
||||
import { Group, Vector3 } from 'three'
|
||||
import { pointAt, tangentAt } from './path'
|
||||
import { useRide } from '../ride/store'
|
||||
|
||||
const ahead = new Vector3()
|
||||
|
||||
// Kenney Car Kit van: body ~2.75 units long natively (wheel-front z=+0.76
|
||||
// to body rear z=-1.4). Scale it up to match the old ~4.6-unit footprint.
|
||||
const VAN_SCALE = 1.7
|
||||
// wheel-front-{left,right} nodes sit at z=+0.76 (native), the body rear at
|
||||
// z=-1.4 — the model's own "nose" points down its local +Z axis. For a
|
||||
// plain (non-camera) Object3D, Object3D.lookAt orients local +Z toward the
|
||||
// look target, so this group's +Z is already the direction of travel after
|
||||
// the lookAt below: the van needs no corrective yaw.
|
||||
const VAN_FORWARD_CORRECTION = 0
|
||||
|
||||
// roof top sits at ~2.3 world units after scaling (body top 1.35 * 1.7)
|
||||
const ROOF_Y = 2.3
|
||||
|
||||
export function Bus() {
|
||||
const ref = useRef<Group>(null)
|
||||
const { scene } = useGLTF('/models/van.glb')
|
||||
useFrame((state) => {
|
||||
const t = useRide.getState().t
|
||||
const g = ref.current!
|
||||
@@ -18,43 +33,19 @@ export function Bus() {
|
||||
})
|
||||
return (
|
||||
<group ref={ref} name="bus">
|
||||
{/* body, two-tone Électron Fou paint job */}
|
||||
<mesh position={[0, 1, 0]}>
|
||||
<boxGeometry args={[2.2, 1.5, 4.6]} />
|
||||
<meshStandardMaterial color="#ff5c00" roughness={0.4} />
|
||||
</mesh>
|
||||
<mesh position={[0, 1.9, 0.3]}>
|
||||
<boxGeometry args={[2.2, 0.5, 3.4]} />
|
||||
<meshStandardMaterial color="#fff3d6" roughness={0.5} />
|
||||
</mesh>
|
||||
{/* windshield */}
|
||||
<mesh position={[0, 1.35, 2.31]}>
|
||||
<boxGeometry args={[1.8, 0.7, 0.05]} />
|
||||
<meshStandardMaterial color="#7ec8ff" emissive="#153a5e" roughness={0.1} />
|
||||
</mesh>
|
||||
{/* wheels */}
|
||||
{([[-1.1, 1.5], [1.1, 1.5], [-1.1, -1.5], [1.1, -1.5]] as const).map(([x, z], i) => (
|
||||
<mesh key={i} position={[x, 0.45, z]} rotation={[0, 0, Math.PI / 2]}>
|
||||
<cylinderGeometry args={[0.45, 0.45, 0.3, 12]} />
|
||||
<meshStandardMaterial color="#111" roughness={0.9} />
|
||||
</mesh>
|
||||
))}
|
||||
{/* headlights */}
|
||||
{[-0.7, 0.7].map((x, i) => (
|
||||
<mesh key={i} position={[x, 0.9, 2.32]}>
|
||||
<sphereGeometry args={[0.16, 8, 8]} />
|
||||
<meshStandardMaterial color="#fffbe6" emissive="#ffd23f" emissiveIntensity={2} />
|
||||
</mesh>
|
||||
))}
|
||||
{/* crazy antenna: a zap sphere on a pole */}
|
||||
<mesh position={[0.8, 2.6, -1.6]}>
|
||||
<Clone object={scene} scale={VAN_SCALE} rotation={[0, VAN_FORWARD_CORRECTION, 0]} />
|
||||
{/* crazy antenna: a zap sphere on a pole, mounted on the roof — the one
|
||||
fun accent kept from the old hand-built bus */}
|
||||
<mesh position={[0.5, ROOF_Y + 0.5, -1.7]}>
|
||||
<cylinderGeometry args={[0.03, 0.03, 1, 6]} />
|
||||
<meshStandardMaterial color="#999" />
|
||||
</mesh>
|
||||
<mesh position={[0.8, 3.2, -1.6]}>
|
||||
<mesh position={[0.5, ROOF_Y + 1.15, -1.7]}>
|
||||
<sphereGeometry args={[0.18, 8, 8]} />
|
||||
<meshStandardMaterial color="#9be564" emissive="#9be564" emissiveIntensity={2.5} />
|
||||
</mesh>
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
useGLTF.preload('/models/van.glb')
|
||||
|
||||
+31
-1
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Instance, Instances } from '@react-three/drei'
|
||||
import { Clone, Instance, Instances, useGLTF } from '@react-three/drei'
|
||||
import { TubeGeometry, Vector3 } from 'three'
|
||||
import { busPath, pointAt, tangentAt } from './path'
|
||||
import { mulberry32 } from './rng'
|
||||
@@ -10,6 +10,28 @@ interface Item {
|
||||
scale: [number, number, number]
|
||||
}
|
||||
|
||||
interface Lamp {
|
||||
pos: [number, number, number]
|
||||
rotY: number
|
||||
}
|
||||
|
||||
const LAMP_TS = [0.06, 0.18, 0.3, 0.42, 0.55, 0.68, 0.8, 0.92]
|
||||
const LAMP_SCALE = 6
|
||||
const LAMP_OFFSET = 5
|
||||
|
||||
function lampLayout(): Lamp[] {
|
||||
return LAMP_TS.map((t, i) => {
|
||||
const p = pointAt(t)
|
||||
const tan = tangentAt(t)
|
||||
const n = new Vector3(-tan.z, 0, tan.x) // unit normal, tangent is already unit length
|
||||
const side = i % 2 === 0 ? 1 : -1
|
||||
const pos = p.clone().addScaledVector(n, side * LAMP_OFFSET)
|
||||
// face the lamp's curved arm back toward the road regardless of path curvature
|
||||
const rotY = Math.atan2(side * n.x, side * n.z)
|
||||
return { pos: [pos.x, 0, pos.z], rotY }
|
||||
})
|
||||
}
|
||||
|
||||
function layout(seed: number, count: number, minOff: number, maxOff: number): Item[] {
|
||||
const rng = mulberry32(seed)
|
||||
const items: Item[] = []
|
||||
@@ -35,7 +57,9 @@ export function World() {
|
||||
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 lamps = useMemo(() => lampLayout(), [])
|
||||
const trace = useMemo(() => new TubeGeometry(busPath, 200, 0.5, 6, false), [])
|
||||
const { scene: lampScene } = useGLTF('/models/light-curved.glb')
|
||||
|
||||
return (
|
||||
<group>
|
||||
@@ -80,6 +104,12 @@ export function World() {
|
||||
<Instance key={i} position={[it.pos[0], 0.05, it.pos[2]]} />
|
||||
))}
|
||||
</Instances>
|
||||
{/* street lamps lining the road */}
|
||||
{lamps.map((l, i) => (
|
||||
<Clone key={i} object={lampScene} position={l.pos} rotation={[0, l.rotY, 0]} scale={LAMP_SCALE} />
|
||||
))}
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
useGLTF.preload('/models/light-curved.glb')
|
||||
|
||||
@@ -1,14 +1,46 @@
|
||||
import { useRef } from 'react'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import { Clone, useGLTF } from '@react-three/drei'
|
||||
import { Mesh, MeshStandardMaterial } from 'three'
|
||||
import { StopPanel, stopAnchor } from './StopPanel'
|
||||
import { STOPS } from '../../content/stops'
|
||||
import { MUSIC } from '../../content/music'
|
||||
|
||||
// Kenney furniture-kit pieces are authored with a corner origin (spanning
|
||||
// +X, +Y, -Z from [0,0,0]) rather than a centered pivot — these helpers
|
||||
// re-center each clone at a given world position.
|
||||
function centered(w: number, h: number, d: number, x: number, yBottom: number, z: number): [number, number, number] {
|
||||
return [x - w / 2, yBottom, z + d / 2]
|
||||
}
|
||||
|
||||
const SPEAKER_SCALE = 5
|
||||
const SPK_W = 0.148 * SPEAKER_SCALE
|
||||
const SPK_H = 0.6364 * SPEAKER_SCALE
|
||||
const SPK_D = 0.148 * SPEAKER_SCALE
|
||||
const COL_GAP = SPK_W * 1.08
|
||||
const COL_X = [-COL_GAP / 2, COL_GAP / 2]
|
||||
const ROW_Y = [0, SPK_H]
|
||||
const STACK_TOP = ROW_Y[1] + SPK_H
|
||||
|
||||
const SMALL_SCALE = 4.4
|
||||
const SMALL_W = 0.148 * SMALL_SCALE
|
||||
const SMALL_D = 0.1332 * SMALL_SCALE
|
||||
|
||||
const GEAR_SCALE = 3.2
|
||||
const RADIO_W = 0.315 * GEAR_SCALE
|
||||
const RADIO_D = 0.0975 * GEAR_SCALE
|
||||
const TV_W = 0.41 * GEAR_SCALE
|
||||
const TV_D = 0.27 * GEAR_SCALE
|
||||
|
||||
const TUBE_Y = STACK_TOP + 1.0
|
||||
|
||||
export function StopElectronFou() {
|
||||
const tubes = useRef<Array<Mesh | null>>([])
|
||||
const needles = useRef<Array<Mesh | null>>([])
|
||||
const ampPos = stopAnchor(STOPS[2], 12)
|
||||
const ampPos = stopAnchor(STOPS[2], 8)
|
||||
const { scene: speakerScene } = useGLTF('/models/speaker.glb')
|
||||
const { scene: speakerSmallScene } = useGLTF('/models/speakerSmall.glb')
|
||||
const { scene: radioScene } = useGLTF('/models/radio.glb')
|
||||
const { scene: tvScene } = useGLTF('/models/televisionVintage.glb')
|
||||
|
||||
useFrame((state) => {
|
||||
const time = state.clock.elapsedTime
|
||||
@@ -17,9 +49,6 @@ export function StopElectronFou() {
|
||||
const mat = m.material as MeshStandardMaterial
|
||||
mat.emissiveIntensity = 1.4 + Math.sin(time * 4 + i * 1.7) * 0.9
|
||||
})
|
||||
needles.current.forEach((m, i) => {
|
||||
if (m) m.rotation.z = Math.sin(time * 6 + i) * 0.5
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -29,35 +58,48 @@ export function StopElectronFou() {
|
||||
<p>{MUSIC.blurb}</p>
|
||||
<a className="sign" href={MUSIC.listenUrl}>♫ {MUSIC.listenLabel}</a>
|
||||
</StopPanel>
|
||||
{/* the giant tube amp behind the panel */}
|
||||
{/* the wall of speakers behind the panel */}
|
||||
<group position={ampPos}>
|
||||
<mesh position={[0, 2.2, 0]}>
|
||||
<boxGeometry args={[9, 4.4, 3.2]} />
|
||||
<meshStandardMaterial color="#17171c" roughness={0.5} />
|
||||
</mesh>
|
||||
<mesh position={[0, 2.2, 1.65]}>
|
||||
<boxGeometry args={[8.2, 3.4, 0.1]} />
|
||||
<meshStandardMaterial color="#3b2c1a" roughness={0.9} />
|
||||
</mesh>
|
||||
{[-3.2, -1.1, 1.1, 3.2].map((x, i) => (
|
||||
<mesh key={i} ref={(el) => { tubes.current[i] = el }} position={[x, 5.2, 0]}>
|
||||
<cylinderGeometry args={[0.5, 0.6, 1.8, 10]} />
|
||||
{ROW_Y.map((y, ri) =>
|
||||
COL_X.map((x, ci) => (
|
||||
<Clone
|
||||
key={`${ri}-${ci}`}
|
||||
object={speakerScene}
|
||||
scale={SPEAKER_SCALE}
|
||||
position={centered(SPK_W, SPK_H, SPK_D, x, y, 0)}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
<Clone
|
||||
object={speakerSmallScene}
|
||||
scale={SMALL_SCALE}
|
||||
position={centered(SMALL_W, 0, SMALL_D, 0, STACK_TOP, 0.45)}
|
||||
/>
|
||||
<Clone
|
||||
object={radioScene}
|
||||
scale={GEAR_SCALE}
|
||||
position={centered(RADIO_W, 0, RADIO_D, -0.55, STACK_TOP, 0)}
|
||||
rotation={[0, 0.3, 0]}
|
||||
/>
|
||||
<Clone
|
||||
object={tvScene}
|
||||
scale={GEAR_SCALE}
|
||||
position={centered(TV_W, 0, TV_D, 0.55, STACK_TOP, 0)}
|
||||
rotation={[0, -0.4, 0]}
|
||||
/>
|
||||
{/* pulsing emissive tubes rising behind the stack — demoscene heartbeat */}
|
||||
{[-0.9, -0.32, 0.32, 0.9].map((x, i) => (
|
||||
<mesh key={i} ref={(el) => { tubes.current[i] = el }} position={[x, TUBE_Y, -0.5]}>
|
||||
<cylinderGeometry args={[0.11, 0.14, 2.2, 10]} />
|
||||
<meshStandardMaterial color="#331100" emissive="#ff5c00" emissiveIntensity={1.4} transparent opacity={0.92} />
|
||||
</mesh>
|
||||
))}
|
||||
{[-2, 2].map((x, i) => (
|
||||
<group key={i} position={[x, 2.2, 1.75]}>
|
||||
<mesh>
|
||||
<boxGeometry args={[1.6, 1, 0.05]} />
|
||||
<meshStandardMaterial color="#f5e6c8" emissive="#4a4230" />
|
||||
</mesh>
|
||||
<mesh ref={(el) => { needles.current[i] = el }} position={[0, -0.2, 0.05]}>
|
||||
<boxGeometry args={[0.05, 0.7, 0.02]} />
|
||||
<meshStandardMaterial color="#e03131" />
|
||||
</mesh>
|
||||
</group>
|
||||
))}
|
||||
</group>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
useGLTF.preload('/models/speaker.glb')
|
||||
useGLTF.preload('/models/speakerSmall.glb')
|
||||
useGLTF.preload('/models/radio.glb')
|
||||
useGLTF.preload('/models/televisionVintage.glb')
|
||||
|
||||
@@ -1,22 +1,61 @@
|
||||
import { StopPanel } from './StopPanel'
|
||||
import { useMemo } from 'react'
|
||||
import { Clone, useGLTF } from '@react-three/drei'
|
||||
import { StopPanel, stopAnchor } from './StopPanel'
|
||||
import { STOPS } from '../../content/stops'
|
||||
import { GREETINGS } from '../../content/greetings'
|
||||
import { useRide } from '../../ride/store'
|
||||
|
||||
// "end of the road" wink: cones + barriers clustered near the terminus
|
||||
const CONES = [
|
||||
{ dx: 3, dz: 1, scale: 1.4, rotY: 0.2 },
|
||||
{ dx: 3.8, dz: -0.4, scale: 1.2, rotY: 1.1 },
|
||||
{ dx: 2.6, dz: -1.6, scale: 1.5, rotY: 2.4 },
|
||||
] as const
|
||||
const BARRIERS = [
|
||||
{ dx: 4.6, dz: 0.4, scale: 7.5, rotY: -0.4 },
|
||||
{ dx: 4.9, dz: -1.4, scale: 7.2, rotY: 0.6 },
|
||||
] as const
|
||||
|
||||
export function StopGreetings() {
|
||||
const base = useMemo(() => stopAnchor(STOPS[3]), [])
|
||||
const { scene: coneScene } = useGLTF('/models/cone.glb')
|
||||
const { scene: barrierScene } = useGLTF('/models/construction-barrier.glb')
|
||||
return (
|
||||
<StopPanel stop={STOPS[3]} width={380}>
|
||||
<h2>Greetings</h2>
|
||||
<p>Respect éternel à :</p>
|
||||
<ul>
|
||||
{GREETINGS.map((g) => (
|
||||
<li key={g}>{g}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="muted">Code + design : Clément Saillant. Aucun cookie n'a été blessé.</p>
|
||||
<button className="sign" onClick={() => useRide.getState().reset()}>
|
||||
⟲ RIDE AGAIN ?
|
||||
</button>
|
||||
</StopPanel>
|
||||
<>
|
||||
<StopPanel stop={STOPS[3]} width={380}>
|
||||
<h2>Greetings</h2>
|
||||
<p>Respect éternel à :</p>
|
||||
<ul>
|
||||
{GREETINGS.map((g) => (
|
||||
<li key={g}>{g}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="muted">Code + design : Clément Saillant. Aucun cookie n'a été blessé.</p>
|
||||
<button className="sign" onClick={() => useRide.getState().reset()}>
|
||||
⟲ RIDE AGAIN ?
|
||||
</button>
|
||||
</StopPanel>
|
||||
{CONES.map((c, i) => (
|
||||
<Clone
|
||||
key={i}
|
||||
object={coneScene}
|
||||
position={[base.x + c.dx, base.y, base.z + c.dz]}
|
||||
rotation={[0, c.rotY, 0]}
|
||||
scale={c.scale}
|
||||
/>
|
||||
))}
|
||||
{BARRIERS.map((b, i) => (
|
||||
<Clone
|
||||
key={i}
|
||||
object={barrierScene}
|
||||
position={[base.x + b.dx, base.y, base.z + b.dz]}
|
||||
rotation={[0, b.rotY, 0]}
|
||||
scale={b.scale}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
useGLTF.preload('/models/cone.glb')
|
||||
useGLTF.preload('/models/construction-barrier.glb')
|
||||
|
||||
@@ -1,16 +1,41 @@
|
||||
import { StopPanel } from './StopPanel'
|
||||
import { useMemo } from 'react'
|
||||
import { Clone, useGLTF } from '@react-three/drei'
|
||||
import { StopPanel, stopAnchor } from './StopPanel'
|
||||
import { STOPS } from '../../content/stops'
|
||||
import { LINKS } from '../../content/links'
|
||||
|
||||
// small decorative sign forest behind the panel — décor only, the real
|
||||
// links stay in the Html panel below
|
||||
const FOREST = [
|
||||
{ dx: -3.5, dz: -2.5, scale: 4.2, rotY: 0.4 },
|
||||
{ dx: 4, dz: -3.5, scale: 5.5, rotY: -0.9 },
|
||||
{ dx: 1.5, dz: -5, scale: 3.4, rotY: 1.8 },
|
||||
] as const
|
||||
|
||||
export function StopIdentities() {
|
||||
const base = useMemo(() => stopAnchor(STOPS[1]), [])
|
||||
const { scene } = useGLTF('/models/sign-highway.glb')
|
||||
return (
|
||||
<StopPanel stop={STOPS[1]} width={380}>
|
||||
<h2>Identités</h2>
|
||||
{LINKS.map((l) => (
|
||||
<a key={l.id} className="sign" href={l.href} {...(l.rel ? { rel: l.rel } : {})}>
|
||||
→ {l.label}
|
||||
</a>
|
||||
<>
|
||||
<StopPanel stop={STOPS[1]} width={380}>
|
||||
<h2>Identités</h2>
|
||||
{LINKS.map((l) => (
|
||||
<a key={l.id} className="sign" href={l.href} {...(l.rel ? { rel: l.rel } : {})}>
|
||||
→ {l.label}
|
||||
</a>
|
||||
))}
|
||||
</StopPanel>
|
||||
{FOREST.map((f, i) => (
|
||||
<Clone
|
||||
key={i}
|
||||
object={scene}
|
||||
position={[base.x + f.dx, base.y, base.z + f.dz]}
|
||||
rotation={[0, f.rotY, 0]}
|
||||
scale={f.scale}
|
||||
/>
|
||||
))}
|
||||
</StopPanel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
useGLTF.preload('/models/sign-highway.glb')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { Html, Text } from '@react-three/drei'
|
||||
import { Clone, Html, Text, useGLTF } from '@react-three/drei'
|
||||
import { Vector3 } from 'three'
|
||||
import type { Stop } from '../../content/stops'
|
||||
import { pointAt, tangentAt } from '../path'
|
||||
@@ -13,6 +13,31 @@ export function stopAnchor(stop: Stop, lateral = 6): Vector3 {
|
||||
return p.clone().addScaledVector(n, lateral)
|
||||
}
|
||||
|
||||
// sign-highway-wide.glb: twin posts (0..0.707 tall) framing a board around
|
||||
// the 0.41-0.53 band, board front face on its native -X side. Scaled up so
|
||||
// the board reads clearly above the Html panel (which stays untouched).
|
||||
const SIGN_SCALE = 8
|
||||
const BOARD_Y = 0.5 * SIGN_SCALE
|
||||
const BOARD_Z = 0.11 * SIGN_SCALE
|
||||
|
||||
const LAMP_SCALE = 6
|
||||
|
||||
// small deterministic hash so each stop's lamp gets a stable yaw variation
|
||||
function hash(id: string): number {
|
||||
let h = 0
|
||||
for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0
|
||||
return h
|
||||
}
|
||||
|
||||
// face the lamp's curved arm back toward the road, plus a per-stop wobble
|
||||
function lampYaw(stop: Stop): number {
|
||||
const tan = tangentAt(stop.t)
|
||||
const n = new Vector3(-tan.z, 0, tan.x)
|
||||
const base = Math.atan2(n.x, n.z)
|
||||
const wobble = ((hash(stop.id) % 1000) / 1000 - 0.5) * 0.6
|
||||
return base + wobble
|
||||
}
|
||||
|
||||
export function StopPanel({ stop, children, width = 340 }: {
|
||||
stop: Stop
|
||||
children: ReactNode
|
||||
@@ -20,24 +45,24 @@ export function StopPanel({ stop, children, width = 340 }: {
|
||||
}) {
|
||||
const pos = useMemo(() => stopAnchor(stop), [stop])
|
||||
const active = useRide((s) => s.stop?.id === stop.id)
|
||||
const { scene: signScene } = useGLTF('/models/sign-highway-wide.glb')
|
||||
const { scene: lampScene } = useGLTF('/models/light-curved.glb')
|
||||
return (
|
||||
<group position={pos}>
|
||||
{/* pole */}
|
||||
<mesh position={[0, 2.5, 0]}>
|
||||
<cylinderGeometry args={[0.12, 0.12, 5, 8]} />
|
||||
<meshStandardMaterial color="#c0c0c0" metalness={0.8} roughness={0.3} />
|
||||
</mesh>
|
||||
{/* bus-stop sign */}
|
||||
<mesh position={[0, 5.4, 0]}>
|
||||
<boxGeometry args={[3.6, 1.2, 0.15]} />
|
||||
<meshStandardMaterial color="#ffd23f" emissive="#453400" />
|
||||
</mesh>
|
||||
<Text position={[0, 5.4, 0.12]} fontSize={0.55} color="#05010d" anchorX="center" anchorY="middle">
|
||||
{/* bus-stop sign: billboard-on-posts (native board faces its local
|
||||
-X; rotate 90° so the face reads along +Z toward the road) */}
|
||||
<Clone object={signScene} scale={SIGN_SCALE} rotation={[0, Math.PI / 2, 0]} />
|
||||
<Text position={[0, BOARD_Y, BOARD_Z + 0.05]} fontSize={0.6} color="#05010d" anchorX="center" anchorY="middle">
|
||||
{stop.title}
|
||||
</Text>
|
||||
<Html transform position={[0, 2.6, 0.4]} distanceFactor={10} style={{ width }}>
|
||||
<div className={`panel${active ? ' panel--active' : ''}`}>{children}</div>
|
||||
</Html>
|
||||
{/* street lamp beside the stop */}
|
||||
<Clone object={lampScene} scale={LAMP_SCALE} position={[5.2, 0, -1]} rotation={[0, lampYaw(stop), 0]} />
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
useGLTF.preload('/models/sign-highway-wide.glb')
|
||||
useGLTF.preload('/models/light-curved.glb')
|
||||
|
||||
Reference in New Issue
Block a user