diff --git a/CLAUDE_REPORT.md b/CLAUDE_REPORT.md index 3bf6066..52cbcf9 100644 --- a/CLAUDE_REPORT.md +++ b/CLAUDE_REPORT.md @@ -11,12 +11,12 @@ The PhysicsHub codebase is a well-structured interactive physics simulation platform. The core architecture (centralized `PhysicsBody`, `ForceCalculator`, `ForceRenderer`, `DragController`) is thoughtfully designed. However, the audit identified **23 confirmed bugs and quality issues** across 4 severity tiers. -| Severity | Count | Key Area | -|---|---|---| -| ๐Ÿ”ด Critical | 3 | Physics correctness, broken physics engine | -| ๐ŸŸ  High | 5 | Integration bugs, null crashes, broken reset | -| ๐ŸŸก Medium | 9 | Code quality, dead code, unit errors | -| ๐ŸŸข Low | 6 | Performance, UX, naming | +| Severity | Count | Key Area | +| ----------- | ----- | -------------------------------------------- | +| ๐Ÿ”ด Critical | 3 | Physics correctness, broken physics engine | +| ๐ŸŸ  High | 5 | Integration bugs, null crashes, broken reset | +| ๐ŸŸก Medium | 9 | Code quality, dead code, unit errors | +| ๐ŸŸข Low | 6 | Performance, UX, naming | --- @@ -50,11 +50,13 @@ All physics runs in **Y-UP** (standard physics: y increases upward). Rendering c The file contains the full source of `Spring.ts` (TypeScript `interface` declarations, class definition, methods) appended directly after `export default PhysicsBody`. This creates a `.js` file with TypeScript syntax, which will crash in any environment that does not transpile `.js` files as TypeScript. Two separate modules should not share one file. **Fix:** + ```js // PhysicsBody.js โ€” everything after this line must be removed: export default PhysicsBody; // โ† file ends here. Spring.ts content below must be deleted. ``` + Ensure `Spring.ts` is the sole location for the Spring class. --- @@ -68,13 +70,24 @@ Ensure `Spring.ts` is the sole location for the Spring class. ```js // SpringConnection.jsx โ€” BUGGY direct call: -renderer.drawVector(p, screenPos.x, screenPos.y, - gravityForce.x, gravityForce.y, // gravityForce.y = -mass*g โ†’ draws UP on screen - "#ef4444", "Weight"); +renderer.drawVector( + p, + screenPos.x, + screenPos.y, + gravityForce.x, + gravityForce.y, // gravityForce.y = -mass*g โ†’ draws UP on screen + "#ef4444", + "Weight" +); // FIX โ€” use the dedicated helper which handles direction correctly: -renderer.drawWeight(p, screenPos.x, screenPos.y, - bodyRef.current.params.mass, inputsRef.current.gravity); +renderer.drawWeight( + p, + screenPos.x, + screenPos.y, + bodyRef.current.params.mass, + inputsRef.current.gravity +); ``` --- @@ -87,7 +100,7 @@ renderer.drawWeight(p, screenPos.x, screenPos.y, ```js const scale = 0; //getTimeScale(); โ† DEBUG LEFTOVER, never removed if (!isPaused()) { - accumulator += dt * Math.max(0, scale); // accumulator always stays 0 + accumulator += dt * Math.max(0, scale); // accumulator always stays 0 } // worldRef.current.step() is never called โ†’ Planck.js body never moves ``` @@ -95,6 +108,7 @@ if (!isPaused()) { The `getTimeScale()` call was commented out. The physics body is created and displayed but completely frozen regardless of user interaction. **Fix:** + ```js import { getTimeScale } from "../app/(core)/constants/Time.js"; // ... @@ -115,6 +129,7 @@ const scale = getTimeScale(); // restore this line The method signature `stepAlongPlane(dt, netForceParallel)` ignores the `angleRad` argument passed by the caller. The 2D `state.velocity` is set as `(velAlongPlane, 0)` instead of being projected onto the plane's actual direction. This makes `state.velocity` incorrect for energy calculations. **Fix:** + ```js stepAlongPlane(dt, netForceParallel, angleRad = 0) { if (dt <= 0) return; @@ -140,12 +155,14 @@ stepAlongPlane(dt, netForceParallel, angleRad = 0) { ```js // BUGGY โ€” measures angle from (0,0), not from the anchor pivot -const angle = (Math.atan2(bodyState.position.x, -bodyState.position.y) * 180) / Math.PI; +const angle = + (Math.atan2(bodyState.position.x, -bodyState.position.y) * 180) / Math.PI; ``` The anchor is at `(w/2 meters, h*0.2 meters)` in physics space โ€” not at origin. This formula produces completely wrong angle values. The `PendulumBody.getAngle()` method is already correct and should be used instead. **Fix:** Pass `body.getAngle()` directly into `SimInfoMapper` via the state object: + ```js // In sketch p.draw(): updateSimInfo(p, { @@ -192,6 +209,7 @@ potentialEnergy: bodyRef.current.getPotentialEnergy(gravity, toMeters(p.height)) In Y-up coordinates, the ground is at `y = 0`. `toMeters(p.height)` is the **canvas top** in meters (a large positive number). So `PE = mass * g * (position.y - large_number)` is almost always large and negative โ€” incorrect. **Fix:** + ```js // Ground reference in Y-up space is y = 0 (or ball radius for center-of-mass) potentialEnergy: bodyRef.current.getPotentialEnergy(gravity, 0), @@ -215,6 +233,7 @@ body.applyForce(force); When the spring is extended (`displacement > 0`), `springForceMag` is negative, which flips the direction vector to point **away** from the anchor. An extended spring should pull the body toward the anchor. **Fix:** + ```ts public connect(body: PhysicsBody): void { const toAnchor = p5.Vector.sub(this.anchor, body.state.position); @@ -301,6 +320,7 @@ friction = ForceCalculator.kineticFriction(normal, frictionKinetic, vel); Should use `body.planeState.velAlongPlane` (the scalar velocity along the plane). `state.velocity.x` is only the horizontal projection, underestimating friction when the angle is non-zero. **Fix:** + ```js const vel = body.planeState?.velAlongPlane ?? body.state.velocity?.x ?? 0; ``` @@ -340,53 +360,59 @@ The `+` operation draws from absolute screen coordinates (origin = top-left corn --- ### BUG-018 โ€” package.json: engines.node uses exact version instead of range + ```json "engines": { "node": "24.8.0" } // should be ">=24.0.0" ``` ### BUG-019 โ€” Config.js: Mars gravity label shows 3.71 m/sยฒ (actual: 3.72 m/sยฒ) + ```js { value: 0.379 * earthG, label: "Mars (3.71 m/sยฒ)" } // 0.379 * 9.81 = 3.719 โ‰ˆ 3.72, label should read "Mars (3.72 m/sยฒ)" ``` ### BUG-020 โ€” VectorsOperations.jsx: "dot" and "cross" case renderers are identical + Both cases draw the exact same three lines. The cross product should show a shaded parallelogram (area = |Aร—B|), the dot product should show a projection onto A. ### BUG-021 โ€” test.jsx: Collision separation check has inverted sign condition + ```js if (velAlongNormal < 0) continue; // "don't resolve if separating" // relVel ยท normal > 0 means separating โ†’ should be: if (velAlongNormal > 0) continue ``` ### BUG-022 โ€” BallGravity.jsx: isBlowing state set but wind-overlay CSS class likely missing + `setIsBlowing(true/false)` updates state and toggles class `wind-overlay blowing`, but no CSS for this class was found in the uploaded styles. The wind animation has no visual effect. ### BUG-023 โ€” DragController.js: Same file-concatenation artifact as BUG-001 + The `DragController.js` file content contains appended source from other files (similar to BUG-001). Each physics module file should contain only its own source. --- ## Priority Fix Order -| # | Bug | File | Effort | -|---|---|---|---| -| 1 | BUG-001, BUG-023 | Separate concatenated files | 30 min | -| 2 | BUG-003 | Restore getTimeScale() | 5 min | -| 3 | BUG-008 | Fix Spring.connect() sign | 15 min | -| 4 | BUG-002 | Fix weight vector direction | 1 hr | -| 5 | BUG-007 | Fix PE reference height | 30 min | -| 6 | BUG-004 | Fix stepAlongPlane 2D projection | 1 hr | -| 7 | BUG-006 | Remove hardcoded reset dimensions | 15 min | -| 8 | BUG-005 | Fix pendulum angle display | 1 hr | -| 9 | BUG-012 | Fix gravity hardcoding in renderer | 30 min | -| 10 | BUG-014 | Fix kinetic friction velocity source | 15 min | -| 11 | BUG-011 | Move SimplePendulum config | 30 min | -| 12 | BUG-021 | Fix collision sign | 5 min | -| 13 | BUG-013 | Add cleanupInstance on unmount | 30 min | -| 14 | BUG-010 | Fix drag label unit | 5 min | -| 15 | BUG-015 | Remove/migrate dead utils | 15 min | -| 16 | All others | Remaining low-priority | ~2 hr | +| # | Bug | File | Effort | +| --- | ---------------- | ------------------------------------ | ------ | +| 1 | BUG-001, BUG-023 | Separate concatenated files | 30 min | +| 2 | BUG-003 | Restore getTimeScale() | 5 min | +| 3 | BUG-008 | Fix Spring.connect() sign | 15 min | +| 4 | BUG-002 | Fix weight vector direction | 1 hr | +| 5 | BUG-007 | Fix PE reference height | 30 min | +| 6 | BUG-004 | Fix stepAlongPlane 2D projection | 1 hr | +| 7 | BUG-006 | Remove hardcoded reset dimensions | 15 min | +| 8 | BUG-005 | Fix pendulum angle display | 1 hr | +| 9 | BUG-012 | Fix gravity hardcoding in renderer | 30 min | +| 10 | BUG-014 | Fix kinetic friction velocity source | 15 min | +| 11 | BUG-011 | Move SimplePendulum config | 30 min | +| 12 | BUG-021 | Fix collision sign | 5 min | +| 13 | BUG-013 | Add cleanupInstance on unmount | 30 min | +| 14 | BUG-010 | Fix drag label unit | 5 min | +| 15 | BUG-015 | Remove/migrate dead utils | 15 min | +| 16 | All others | Remaining low-priority | ~2 hr | **Estimated total fix time: ~10 hours** @@ -406,4 +432,4 @@ The `DragController.js` file content contains appended source from other files ( --- -*Report generated by static analysis of repository snapshot `physicshub.github.io-main`.* +_Report generated by static analysis of repository snapshot `physicshub.github.io-main`._ diff --git a/package.json b/package.json index 3922968..23add76 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "husky": "^9.1.7", "lint-staged": "^16.2.7", "nodemon": "^3.1.10", - "prettier": "3.8.0", + "prettier": "^3.8.0", "puppeteer": "^24.34.0", "sitemap": "^9.0.1", "tailwindcss": "^4", diff --git a/simulations/ParabolicMotion.jsx b/simulations/ParabolicMotion.jsx index 97aedde..4e1bc5e 100644 --- a/simulations/ParabolicMotion.jsx +++ b/simulations/ParabolicMotion.jsx @@ -88,7 +88,7 @@ export default function ParabolicMotion() { trailLayerRef.current.background(r, g, b); }; - const buildTrajectoryPoints = (analytics, startPos, gravity, radius) => { + const buildTrajectoryPoints = (analytics, startPos, gravity) => { if (!analytics || !startPos) return []; if (!isFinite(analytics.flightTime) || analytics.flightTime <= 0) { return []; @@ -96,18 +96,15 @@ export default function ParabolicMotion() { const points = []; const dt = analytics.flightTime / TRAJECTORY_STEPS; - const canvasHeightMeters = toMeters(p.height); - const ground = canvasHeightMeters - radius; for (let i = 0; i <= TRAJECTORY_STEPS; i++) { const t = dt * i; const x = startPos.x + analytics.vx0 * t; - const y = startPos.y - analytics.vy0 * t + 0.5 * gravity * t * t; + const y = startPos.y + analytics.vy0 * t + 0.5 * -gravity * t * t; - if (y > ground) break; points.push({ x: toPixels(x), - y: toPixels(y), + y: p.height - toPixels(y), }); } return points; @@ -130,11 +127,14 @@ export default function ParabolicMotion() { const canvasWidthMeters = toMeters(p.width); const radius = size / 2; - // Clamp height to valid range - const safeHeight = Math.min( - Math.max(h0, 0), - canvasHeightMeters - radius * 2 - ); + // Ground for center-based system + const groundY = canvasHeightMeters - radius; + + // Clamp launch height to be between 0 and ground height + const safeHeight = Math.max(radius, h0); + + // Convert height-above-ground โ†’ world Y (downward-positive) + const startY = safeHeight; // Compute projectile analytics const analytics = computeProjectileAnalytics({ @@ -146,15 +146,10 @@ export default function ParabolicMotion() { // Starting position const startX = Math.max(toMeters(80), canvasWidthMeters * 0.12); - const groundY = canvasHeightMeters; - const startY = Math.min( - groundY - radius, - Math.max(radius, groundY - safeHeight - radius) - ); // Set initial velocity const vx0 = analytics.vx0; - const vy0World = -analytics.vy0; // Convert to downward-positive axis + const vy0World = analytics.vy0; // Convert to downward-positive axis bodyRef.current.state.position.set(startX, startY); bodyRef.current.state.velocity.set(vx0, vy0World); @@ -226,8 +221,9 @@ export default function ParabolicMotion() { p.draw = () => { if (!bodyRef.current) return; - const dt = computeDelta(p); - if (dt <= 0) return; + const fixedDt = 1 / 120; + let frameTime = computeDelta(p); + if (frameTime <= 0) return; // Relaunch if needed if (needsRelaunchRef.current) { @@ -248,47 +244,59 @@ export default function ParabolicMotion() { bodyRef.current.trail.enabled = trailEnabled; bodyRef.current.trail.color = inputsRef.current.ballColor; - // Apply forces (if not dragging) - if (!dragControllerRef.current.isDragging()) { - // Gravity - const gravityForce = ForceCalculator.gravity( - bodyRef.current.params.mass, - gravity - ); - bodyRef.current.applyForce( - p.createVector(gravityForce.x, gravityForce.y) - ); + while (frameTime > 0) { + const frameStep = Math.min(frameTime, fixedDt); - // Air resistance - if (dragCoeff > 0) { - const vel = bodyRef.current.state.velocity; - const drag = ForceCalculator.airResistance( - vel.mag(), - dragCoeff, - false // quadratic drag + // Apply forces (if not dragging) + if (!dragControllerRef.current.isDragging()) { + // Gravity + const gravityForce = ForceCalculator.gravity( + bodyRef.current.params.mass, + gravity ); - if (Math.abs(drag) > 0.001) { - const dragForce = vel.copy().normalize().mult(drag); - bodyRef.current.applyForce(dragForce); + bodyRef.current.applyForce( + p.createVector(gravityForce.x, gravityForce.y) + ); + + // Air resistance + if (dragCoeff > 0) { + const vel = bodyRef.current.state.velocity; + const drag = ForceCalculator.airResistance( + vel.mag(), + dragCoeff, + false // quadratic drag + ); + if (Math.abs(drag) > 0.001 && vel.mag() > 0) { + const dragForce = vel.copy().normalize().mult(drag); + bodyRef.current.applyForce(dragForce); + } } - } - // Wind force - if (wind !== 0) { - bodyRef.current.applyForce(p.createVector(wind, 0)); - } + // Wind force + if (wind !== 0) { + bodyRef.current.applyForce(p.createVector(wind, 0)); + } - // Physics step - bodyRef.current.step(dt); + // Physics step + bodyRef.current.step(frameStep); - // Check ground collision - const radius = bodyRef.current.params.size / 2; - const groundY = toMeters(p.height) - radius; - if (bodyRef.current.state.position.y >= groundY) { - bodyRef.current.state.position.y = groundY; - bodyRef.current.state.velocity.y = 0; - bodyRef.current.state.velocity.x *= 0.95; // Ground friction + // Check ground collision + + const radius = bodyRef.current.params.size / 2; + const groundY = radius; + + if (bodyRef.current.state.position.y <= groundY) { + bodyRef.current.state.position.y = groundY; + + if (bodyRef.current.state.velocity.y < 0) { + bodyRef.current.state.velocity.y = 0; + } + + bodyRef.current.state.velocity.x *= 0.95; // Ground friction + } } + + frameTime -= frameStep; } // Render scene @@ -396,7 +404,7 @@ export default function ParabolicMotion() { p.push(); p.stroke(100, 100, 120); p.strokeWeight(2); - p.line(0, p.height - 2, p.width, p.height - 2); + p.line(0, p.height, p.width, p.height); p.pop(); };