/** * PhysicsPanel.jsx — Full physics control panel * Gravity, wind, global friction/restitution, per-model body properties, * velocity/force controls, telemetry readout, vehicle presets. */ import { useState, useEffect, useRef } from 'react' import useStore from '../store/useStore' import { applyImpulse, applyForce, setBodyVelocity, setAngularVelocity, setConstantForce, getBodyState, teleportBody, getBodies, } from './PhysicsEngine' const COLORS = ['#4f8eff','#ef4444','#22c55e','#f59e0b','#8b5cf6','#f97316'] // ── Reusable slider ───────────────────────────────────────────────────────── function Slider({ label, value, onChange, min=0, max=1, step=0.01, unit='', color='var(--accent)', fmt }) { const disp = fmt ? fmt(value) : (typeof value==='number' ? value.toFixed(step<0.01?3:step<0.1?2:1) : value) return (
{label} {disp}{unit}
onChange(+e.target.value)} />
) } // ── Toggle switch ──────────────────────────────────────────────────────────── function Toggle({ value, onChange, color='var(--accent)' }) { return ( ) } // ── Section collapse ───────────────────────────────────────────────────────── function Sec({ title, color='var(--text2)', children, open:initOpen=true }) { const [open, setOpen] = useState(initOpen) return (
{open &&
{children}
}
) } // ── Telemetry readout ──────────────────────────────────────────────────────── function Telemetry({ modelId }) { const [state, setState] = useState(null) useEffect(() => { if (!modelId) return const iv = setInterval(() => setState(getBodyState(modelId)), 100) return () => clearInterval(iv) }, [modelId]) if (!state) return (
No physics body active for this model
) const speed = state.speed.toFixed(2) const rows = [ ['Speed', `${speed} m/s`], ['Vel X', state.velocity.x.toFixed(3)], ['Vel Y', state.velocity.y.toFixed(3)], ['Vel Z', state.velocity.z.toFixed(3)], ['Pos X', state.position.x.toFixed(2)], ['Pos Y', state.position.y.toFixed(2)], ['Pos Z', state.position.z.toFixed(2)], ['ω X', state.angularVelocity.x.toFixed(3)], ['Sleeping', state.sleeping ? 'YES' : 'no'], ] return (
{rows.map(([k,v]) => (
{k} 0.5?'var(--accent3)':'var(--text1)') : k==='Sleeping' ? (v==='YES'?'var(--text3)':'var(--accent)') : 'var(--text0)' }}> {v}
))}
) } // ── Vehicle preset ──────────────────────────────────────────────────────────── const VEHICLE_PRESETS = { car: { mass:1200, damping:0.3, angularDamping:0.7, friction:0.6, restitution:0.1, centerOfMassY:-0.3, collisionShape:'box', ccdRadius:1 }, truck: { mass:8000, damping:0.5, angularDamping:0.9, friction:0.7, restitution:0.05,centerOfMassY:-0.5, collisionShape:'box', ccdRadius:1 }, motorcycle: { mass:250, damping:0.2, angularDamping:0.4, friction:0.5, restitution:0.1, centerOfMassY:-0.1, collisionShape:'box', ccdRadius:1 }, ball: { mass:1, damping:0.01,angularDamping:0.01,friction:0.2, restitution:0.8, centerOfMassY:0, collisionShape:'sphere' }, box: { mass:50, damping:0.4, angularDamping:0.6, friction:0.5, restitution:0.3, centerOfMassY:0, collisionShape:'box' }, feather: { mass:0.01, damping:0.99,angularDamping:0.99,friction:0.1, restitution:0.1, centerOfMassY:0, collisionShape:'box' }, } // ── Per-model card ──────────────────────────────────────────────────────────── function ModelPhysicsCard({ model, index, physicsEnabled }) { const { modelPhysics, setModelPhysics } = useStore() const props = { mass:1, damping:0.3, angularDamping:0.5, type:'dynamic', friction:0.4, restitution:0.2, staticFriction:0.6, centerOfMassY:0, collisionShape:'box', ccdRadius:0, ...modelPhysics[model.id] } const c = COLORS[index % COLORS.length] const [open, setOpen] = useState(false) const [engineForce, setEngineForce] = useState({ x:0, y:0, z:0 }) const [velocity, setVelocityUI] = useState({ x:0, y:0, z:0 }) const upd = (k, v) => setModelPhysics(model.id, { [k]:v }) const applyPreset = (name) => { const p = VEHICLE_PRESETS[name] if (p) setModelPhysics(model.id, p) } const handleSetVelocity = () => { setBodyVelocity(model.id, velocity) } const handleSetForce = () => { setConstantForce(model.id, (engineForce.x||engineForce.y||engineForce.z) ? engineForce : null) } return (
{/* Header */} {open && (
{/* Presets */}
PRESETS
{Object.keys(VEHICLE_PRESETS).map(name => ( ))}
{/* Body type */}
BODY TYPE
{['dynamic','static','kinematic'].map(t=>( ))}
{/* Collision shape */}
COLLISION SHAPE
{['box','sphere','cylinder'].map(sh=>( ))}
{props.type !== 'static' && <> upd('mass',v)} /> upd('damping',v)} /> upd('angularDamping',v)} /> upd('friction',v)} /> upd('staticFriction',v)} /> upd('restitution',v)} /> upd('centerOfMassY',v)} fmt={v=>(v>0?'+':'')+v.toFixed(2)} unit="m" />
CCD (fast-moving objects) 0} onChange={v=>upd('ccdRadius',v?1:0)} color={c} />
} {/* ── Live controls (requires physics ON) ── */} {physicsEnabled && props.type==='dynamic' && (
LIVE CONTROLS
{/* Impulse buttons */}
{[ ['⬆ Up', ()=>applyImpulse(model.id,{x:0,y:props.mass*5,z:0})], ['→ Right', ()=>applyImpulse(model.id,{x:props.mass*3,y:0,z:0})], ['← Left', ()=>applyImpulse(model.id,{x:-props.mass*3,y:0,z:0})], ['▶ Fwd', ()=>applyImpulse(model.id,{x:0,y:0,z:-props.mass*3})], ['◀ Back', ()=>applyImpulse(model.id,{x:0,y:0,z:props.mass*3})], ['⏹ Stop', ()=>{ setBodyVelocity(model.id,{x:0,y:0,z:0}); setAngularVelocity(model.id,{x:0,y:0,z:0}) }], ].map(([lbl,fn])=>( ))}
{/* Set velocity */}
Set Velocity (m/s)
{['x','y','z'].map(ax=>( setVelocityUI(v=>({...v,[ax]:+e.target.value}))} placeholder={ax.toUpperCase()} style={{ fontSize:11, textAlign:'center' }}/> ))}
{/* Constant engine force */}
Constant Force (N) — engine/motor
{['x','y','z'].map(ax=>( setEngineForce(f=>({...f,[ax]:+e.target.value}))} placeholder={ax.toUpperCase()} style={{ fontSize:11, textAlign:'center' }}/> ))}
{/* Telemetry */}
TELEMETRY
{/* Teleport to origin */}
)}
)}
) } // ── Main PhysicsPanel ───────────────────────────────────────────────────────── export default function PhysicsPanel() { const { physicsEnabled, setPhysicsEnabled, physicsConnected, setPhysicsConnected, gravity, setGravity, physicsConfig, setPhysicsConfig, physicsWind, setPhysicsWind, models, } = useStore() const windSpeed = Math.sqrt((physicsWind.x||0)**2 + (physicsWind.y||0)**2 + (physicsWind.z||0)**2).toFixed(1) return (
{/* ── STEP 1: Enable physics world ── */}
Step 1 Physics Engine
Cannon-es world · 120Hz substeps · gravity & materials
{ setPhysicsEnabled(v) if (!v) setPhysicsConnected(false) // disconnect when world turns off }} />
{physicsEnabled && (
v.toFixed(2)} unit=" m/s²" />
{[['🌍 Earth',-9.82],['🌙 Moon',-1.62],['♂ Mars',-3.72],['🪐 Jupiter',-24.8],['🚀 Zero G',0],['🔄 Reverse',9.82]] .map(([lbl,g])=>( ))}
)}
{/* ── STEP 2: Connect physics to models ── */}
Step 2 Connect to Models
{physicsConnected ? '🟢 Physics bodies active — models are under physics control' : '⚠️ Configure model body types below, then click Connect'}
{physicsEnabled && !physicsConnected && (
💡 Before connecting:
• Set city / ground / buildingsStatic
• Set cars / objectsDynamic
• Adjust mass, friction, etc. per model
• Connecting moves bodies to current model positions
)}
{!physicsConnected ? ( ) : ( )}
{physicsConnected && (
{models.length} bod{models.length !== 1 ? 'ies' : 'y'} active · Disconnect to reposition models normally
)}
{physicsEnabled && physicsConnected !== undefined && <> {/* Global material */} setPhysicsConfig({globalFriction:v})} /> setPhysicsConfig({globalRestitution:v})} />
{[['🧊 Ice',{f:0.02,r:0.05}],['🏖 Sand',{f:1.5,r:0.1}],['🏎 Track',{f:0.8,r:0.2}], ['🏀 Court',{f:0.6,r:0.6}],['🌊 Wet',{f:0.1,r:0.15}]] .map(([lbl,{f,r}])=>( ))}
{/* Wind */}
Wind speed: {windSpeed} N
{['x','y','z'].map((ax,i)=>( setPhysicsWind({[ax]:v})} fmt={v=>(v>0?'+':'')+v.toFixed(1)} unit=" N" /> ))}
{[['Calm',{x:0,y:0,z:0}],['Breeze',{x:5,y:0,z:0}],['Strong',{x:20,y:0,z:0}],['Storm',{x:50,y:0,z:0}]] .map(([lbl,w])=>( ))}
{/* Per-model */}
{models.length} MODEL{models.length!==1?'S':''}
{models.length===0 ? (
Load a model to configure physics
) : ( models.map((m,i) => ( )) )} {/* Info */}
💡 Traffic simulation tips:
• Set road/buildings to Static, vehicles to Dynamic
• Use Car preset for realistic vehicle mass + COM
• Use Constant Force to simulate engine power
• Use Set Velocity for scripted traffic movement
• Enable CCD on fast-moving vehicles to prevent tunneling
• Lower Center of Mass prevents cars from rolling over
}
) }