feat: add option to show constraint forces in pendulum simulation

This commit is contained in:
monoid 2025-08-26 20:44:51 +09:00
parent 0d3b84cc28
commit d16d99d2fd
2 changed files with 18 additions and 3 deletions

View file

@ -80,13 +80,17 @@ export function drawConstraint(ctx: CanvasRenderingContext2D, ball1: BallState,
export function drawWorld(ctx: CanvasRenderingContext2D, export function drawWorld(ctx: CanvasRenderingContext2D,
world: PhysicalWorldState, world: PhysicalWorldState,
ballDrawers: BallDrawer[] ballDrawers: BallDrawer[],
{
showForces = false
} = {}
) { ) {
for (const constraint of world.constraints) { for (const constraint of world.constraints) {
const b1 = world.balls[constraint.p1Idx]; const b1 = world.balls[constraint.p1Idx];
const b2 = world.balls[constraint.p2Idx]; const b2 = world.balls[constraint.p2Idx];
drawConstraint(ctx, b1, b2); drawConstraint(ctx, b1, b2);
// Draw force vectors for debugging // Draw force vectors for debugging
if (!showForces) continue;
const scale = 100; // Scale for visibility const scale = 100; // Scale for visibility
if (constraint.p1ConstraintForce) { if (constraint.p1ConstraintForce) {
ctx.beginPath(); ctx.beginPath();

View file

@ -7,6 +7,7 @@ export default function Pendulum() {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const [subStep, setSubStep] = useState(10); // number of sub-steps per frame const [subStep, setSubStep] = useState(10); // number of sub-steps per frame
const [constraintIterations, setConstraintIterations] = useState(1); // number of constraint iterations per sub-step const [constraintIterations, setConstraintIterations] = useState(1); // number of constraint iterations per sub-step
const [showForces, setShowForces] = useState(false); // whether to show constraint forces
// refs to hold pendulum instances and animation state so we can reset from UI // refs to hold pendulum instances and animation state so we can reset from UI
const worldRef = useRef<WorldState | null>(null); const worldRef = useRef<WorldState | null>(null);
@ -45,7 +46,9 @@ export default function Pendulum() {
} }
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
if (worldRef.current) { if (worldRef.current) {
drawWorld(ctx, worldRef.current, worldRef.current.ballDrawers); drawWorld(ctx, worldRef.current, worldRef.current.ballDrawers, {
showForces,
});
} }
rafRef.current = requestAnimationFrame(animate); rafRef.current = requestAnimationFrame(animate);
}; };
@ -119,7 +122,7 @@ export default function Pendulum() {
window.removeEventListener("pointermove", onPointerMove); window.removeEventListener("pointermove", onPointerMove);
window.removeEventListener("pointerup", onPointerUp); window.removeEventListener("pointerup", onPointerUp);
}; };
}, [constraintIterations, subStep]); }, [constraintIterations, showForces, subStep]);
// reset handler to re-create pendulums and avoid large dt on next frame // reset handler to re-create pendulums and avoid large dt on next frame
const reset = () => { const reset = () => {
@ -159,6 +162,14 @@ export default function Pendulum() {
className="ml-2 w-16 px-2 py-1 border border-gray-300 rounded focus:outline-none" className="ml-2 w-16 px-2 py-1 border border-gray-300 rounded focus:outline-none"
/> />
</label> </label>
<label className="ml-4 text-sm">
:
<input
type="checkbox"
onChange={(e) => setShowForces(e.target.checked)}
className="ml-2 w-4 h-4"
/>
</label>
</div> </div>
<div className="flex-1 min-h-0"> <div className="flex-1 min-h-0">
<canvas <canvas