b3d-radar
A radar sensor you attach to a platform (an aircraft, a turret). It enumerates the scene's radar-blips, detects those in range · profile and inside its cone, and acquires locks on the nearest opposed contacts — all via the pure, tested radar model. Nest it in the thing it rides:
b3dAircraft({ player: true, y: 0 },
b3dRadar({ range: 250, coneDeg: 90, lockTime: 1.5, maxLocks: 2 }))
Demo — watch a lock build and decay
The mast sweeps, so contacts enter and leave the cone on their own. Watch the
lock column: a contact inside the acquire cone climbs toward 1.00, holds while
it stays in the wider maintain cone, and drops the moment it falls out. The
grey contact is faction: 'neutral' — detected, never locked.
import { b3d, b3dRadar, b3dRadarBlip, b3dBox, label3d, slider3d } from 'tosijs-3d'
import { demoStage, orbitCam } from 'tosijs-3d/demo-utils'
import { tosi, elements } from 'tosijs'
const { div } = elements
const { rad } = tosi({ rad: { sweep: 35, range: 60 } })
// The platform: a mast the radar rides. A radar finds what it is nested in.
const radar = b3dRadar({ range: rad.range, coneDeg: 70, acquireConeDeg: 28, lockTime: 1.2, maxLocks: 2 })
const mast = b3dBox({ meshName: 'mast', width: 0.8, depth: 0.8, height: 3, y: 1.5, color: '#8a94a6' }, radar)
// Contacts: each a box carrying a blip, so the blip follows the mesh.
const contacts = [
{ name: 'alpha', faction: 'hostile', profile: 1.4, r: 22, speed: 0.35, color: '#d05050' },
{ name: 'bravo', faction: 'hostile', profile: 0.6, r: 34, speed: -0.22, color: '#d07a50' },
{ name: 'civil', faction: 'neutral', profile: 1.2, r: 27, speed: 0.5, color: '#9aa5b1' },
]
// Keep the BLIP elements: a track's `id` is the blip element itself, not a
// name, so the readout matches by identity. (Stringifying it gives
// "[object HTMLElement]" — which is how I first labelled every row 'alpha'.)
const blips = contacts.map((c) => b3dRadarBlip({ faction: c.faction, profile: c.profile }))
const boxes = contacts.map((c, i) =>
b3dBox({ meshName: c.name, size: 1.2, y: 1, color: c.color, glow: 0.3 }, blips[i]))
const readout = div({ style: 'font:12px ui-monospace,monospace; padding:6px; white-space:pre' })
const scene = b3d(
{
glowLayerIntensity: 0.6,
scenePanel: () => [
label3d({ text: 'radar' }),
slider3d({ label: 'sweep °/s', value: rad.sweep, min: 0, max: 90, step: 5 }),
slider3d({ label: 'range', value: rad.range, min: 15, max: 90, step: 5 }),
],
sceneCreated(el) {
orbitCam(el, { radius: 46, beta: Math.PI / 3.2, target: [0, 1, 0] })
let t = 0
el.scene.registerBeforeRender(() => {
const dt = el.frameDelta
t += dt
// ry is DEGREES — the cone sweeps, so contacts come and go on their own.
mast.ry = (mast.ry + rad.sweep.valueOf() * dt) % 360
radar.range = rad.range.valueOf()
boxes.forEach((b, i) => {
const c = contacts[i]
b.x = Math.cos(t * c.speed + i * 2) * c.r
b.z = Math.sin(t * c.speed + i * 2) * c.r
})
const rows = (radar.tracks ?? []).map((tr) => {
const c = contacts[blips.indexOf(tr.id)] ?? contacts[0]
const bar = '█'.repeat(Math.round(tr.lockProgress * 10)).padEnd(10, '·')
return `${c.name.padEnd(6)} ${String(Math.round(tr.distance)).padStart(3)}m ` +
`${tr.detected ? 'seen' : ' '} ${bar} ${tr.locked ? 'LOCK' : ''}`
})
readout.textContent = rows.length
? 'contact dist det lock\n' + rows.join('\n')
: 'no contacts in cone'
})
},
},
...demoStage({ size: 90, tiles: 18, texture: '/tosi-warhol-testgrid.svg', timeOfDay: 10 }),
mast,
...boxes,
)
preview.append(scene, readout)
The platform reads radar.tracks (to plot the HUD) and radar.nearestLock (a homing
missile's target — no lock ⇒ the missile flies ballistic). A turret wants a cheap
one: maxLocks: 1, alignment: 'hostile' (so it only locks the player).
Attributes
| Attribute | Default | Description |
|---|---|---|
range |
250 |
Nominal range — a profile 1 blip is detected within this. |
coneDeg |
90 |
Detection cone HALF-angle (deg). 90 = front hemisphere. |
lockTime |
1.5 |
Seconds of continuous detection to acquire a lock (0 = instant). |
maxLocks |
2 |
Max simultaneous locks (the nearest opposed contacts). |
alignment |
'friendly' |
This radar's own faction. It locks only opposed blips (friendly⇄hostile); neutrals/waypoints show but never lock. |
updateInterval |
100 |
Radar refresh period (ms). Runs below frame-rate and is dithered across frames so many radars don't all recompute the same frame. |