← Physics Playground results

Physics Playground

thinkingcap qwen3.6 27b · typescript

Initial view of Physics Playground

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 58,873 bytes · SHA-256 c12ddea60781
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>2D Soft-Body Physics Playground</title>
<style>
/* ── css: layout ── */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #1a1a2e;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  user-select: none;
}

#app {
  width: 100%;
  height: 100%;
  position: relative;
}

#sim-canvas {
  display: block;
  cursor: crosshair;
}

/* ─── Toolbar ───────────────────────────────────────────────────── */
#toolbar {
  position: absolute;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  background: rgba(20, 20, 40, 0.85);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  z-index: 10;
}

.tool-btn {
  background: rgba(255, 255, 255, 0.08);
  color: #ccc;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.15s ease;
}

.tool-btn:hover {
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
}

.tool-btn.active {
  background: rgba(78, 205, 196, 0.3);
  border-color: rgba(78, 205, 196, 0.5);
  color: #4ecdc4;
}

.toolbar-sep {
  width: 1px;
  height: 24px;
  background: rgba(255, 255, 255, 0.15);
  margin: 0 4px;
}

.slider {
  width: 70px;
  height: 4px;
  -webkit-appearance: none;
  appearance: none;
  background: rgba(255, 255, 255, 0.15);
  border-radius: 2px;
  outline: none;
}

.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  background: #4ecdc4;
  border-radius: 50%;
  cursor: pointer;
}

.slider::-moz-range-thumb {
  width: 14px;
  height: 14px;
  background: #4ecdc4;
  border-radius: 50%;
  cursor: pointer;
  border: none;
}

.toggle {
  width: 32px;
  height: 18px;
  -webkit-appearance: none;
  appearance: none;
  background: rgba(255, 255, 255, 0.15);
  border-radius: 9px;
  outline: none;
  cursor: pointer;
  position: relative;
  transition: background 0.2s;
}

.toggle:checked {
  background: #4ecdc4;
}

.toggle::before {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  background: white;
  border-radius: 50%;
  transition: transform 0.2s;
}

.toggle:checked::before {
  transform: translateX(14px);
}

label {
  color: #999;
  font-size: 11px;
  cursor: pointer;
}
</style>
</head>
<body>
<div id="app"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./constants":"src/constants.ts","./types":"src/types.ts","./physics":"src/physics.ts","./spawner":"src/spawner.ts","./renderer":"src/renderer.ts","./input":"src/input.ts","./ui":"src/ui.ts"},"src/physics.ts":{"./constants":"src/constants.ts","./types":"src/types.ts"},"src/spawner.ts":{"./constants":"src/constants.ts","./types":"src/types.ts"},"src/renderer.ts":{"./constants":"src/constants.ts","./types":"src/types.ts","./physics":"src/physics.ts"},"src/input.ts":{"./constants":"src/constants.ts","./types":"src/types.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const constants_1 = require("./constants");
const physics_1 = require("./physics");
const spawner_1 = require("./spawner");
const renderer_1 = require("./renderer");
const input_1 = require("./input");
const ui_1 = require("./ui");
const C = constants_1.CONSTANTS;
// ─── Simulation State ──────────────────────────────────────────────
let points = [];
let constraints = [];
let objects = [];
let staticCircles = [];
let staticBoxes = [];
// ─── Canvas & Context ──────────────────────────────────────────────
const canvas = (0, renderer_1.setupCanvas)();
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
// ─── UI ────────────────────────────────────────────────────────────
let toolbar = null;
let paused = false;
let spawnStartX = 0, spawnStartY = 0;
let isSpawningRope = false;
// ─── Timing ────────────────────────────────────────────────────────
let lastTime = 0;
let accumulator = 0;
let fpsCounter = 60;
let stepTimeMs = 0;
let frameCount = 0;
let fpsTimer = 0;
// ─── Input ─────────────────────────────────────────────────────────
const input = (0, input_1.createInputState)();
// ─── Initialize Simulation ─────────────────────────────────────────
function initSim() {
    points = [];
    constraints = [];
    objects = [];
    // Add static geometry: a circle obstacle and a box platform
    const cw = window.innerWidth;
    const ch = window.innerHeight;
    staticCircles = [
        { x: cw * 0.5, y: ch * 0.65, radius: 45 },
        { x: cw * 0.25, y: ch * 0.45, radius: 30 },
    ];
    staticBoxes = [
        { x: cw * 0.7, y: ch * 0.75, w: 120, h: 15 },
    ];
    // Spawn demo objects
    (0, spawner_1.spawnCloth)(points, constraints, objects, cw * 0.3, 60);
    (0, spawner_1.spawnRope)(points, constraints, objects, cw * 0.7, 40, cw * 0.7, 200);
    (0, spawner_1.spawnSoftBody)(points, constraints, objects, cw * 0.5, ch * 0.3);
    (0, spawner_1.spawnBall)(points, constraints, objects, cw * 0.15, 80);
}
// ─── Reset Simulation ──────────────────────────────────────────────
function resetSim() {
    initSim();
}
// ─── Resize Handler ────────────────────────────────────────────────
window.addEventListener('resize', () => {
    (0, renderer_1.resizeCanvas)(canvas);
});
// ─── Main Loop ─────────────────────────────────────────────────────
function gameLoop(timestamp) {
    if (!lastTime)
        lastTime = timestamp;
    let delta = (timestamp - lastTime) / 1000;
    lastTime = timestamp;
    // Clamp delta to prevent explosion on tab-switch
    delta = Math.min(delta, C.maxFrameDelta);
    // FPS counter
    frameCount++;
    fpsTimer += delta;
    if (fpsTimer >= 0.5) {
        fpsCounter = Math.round(frameCount / fpsTimer);
        frameCount = 0;
        fpsTimer = 0;
    }
    const cw = window.innerWidth;
    const ch = window.innerHeight;
    // Update constants from UI sliders
    C.gravity = parseFloat(toolbar?.gravitySlider.value ?? '980');
    C.iterations = parseInt(toolbar?.iterationsSlider.value ?? '5', 10);
    C.stressVisualization = toolbar?.stressToggle.checked ?? false;
    // Fixed timestep accumulator
    if (!paused) {
        accumulator += delta;
        const stepStart = performance.now();
        while (accumulator >= C.dt) {
            (0, physics_1.physicsStep)(points, constraints, staticCircles, staticBoxes, cw, ch, C.gravity);
            accumulator -= C.dt;
        }
        stepTimeMs = performance.now() - stepStart;
    }
    else {
        stepTimeMs = 0;
    }
    // Process input (grab/tear)
    handleInput();
    // Render
    (0, renderer_1.resizeCanvas)(canvas);
    (0, renderer_1.render)(ctx, points, constraints, objects, staticCircles, staticBoxes, cw, ch, input.grabPointIdx, fpsCounter, stepTimeMs, paused);
    requestAnimationFrame(gameLoop);
}
// ─── Input Handling ────────────────────────────────────────────────
function handleInput() {
    // Grab logic (left click)
    if (input.mouseDown && input.grabPointIdx === null && !isSpawningRope) {
        const idx = (0, input_1.findNearestPoint)(points, objects, input.mouseX, input.mouseY, C.grabRadius);
        if (idx !== null) {
            // Grab the point
            input.grabPointIdx = idx;
            input.dragVelX = 0;
            input.dragVelY = 0;
        }
        else if (!paused && toolbar?.selectedTool) {
            // Spawn new object at click position
            spawnObject(toolbar.selectedTool, input.mouseX, input.mouseY);
        }
    }
    // Rope spawning (drag to draw)
    if (isSpawningRope && !input.mouseDown) {
        isSpawningRope = false;
        return;
    }
    // Tear logic (right click or shift+click)
    if (input.rightMouseDown || (input.shiftKey && input.mouseDown)) {
        const cIdx = (0, input_1.findNearestConstraint)(constraints, points, input.mouseX, input.mouseY, C.tearRadius);
        if (cIdx !== null) {
            constraints[cIdx].broken = true;
        }
    }
    // Apply grab position to point
    if (input.grabPointIdx !== null && input.grabPointIdx < points.length) {
        const p = points[input.grabPointIdx];
        p.x = input.mouseX;
        p.y = input.mouseY;
        p.prevX = input.mouseX - input.dragVelX * 3;
        p.prevY = input.mouseY - input.dragVelY * 3;
    }
}
// ─── Spawn Object at Position ──────────────────────────────────────
function spawnObject(tool, x, y) {
    switch (tool) {
        case 'rope':
            isSpawningRope = true;
            spawnStartX = x;
            spawnStartY = y;
            break;
        case 'cloth':
            (0, spawner_1.spawnCloth)(points, constraints, objects, x - C.clothCols * C.clothSpacing / 2, y);
            break;
        case 'softbody':
            (0, spawner_1.spawnSoftBody)(points, constraints, objects, x, y);
            break;
        case 'ball':
            (0, spawner_1.spawnBall)(points, constraints, objects, x, y);
            break;
    }
}
// ─── Keyboard Controls ─────────────────────────────────────────────
window.addEventListener('keydown', (e) => {
    if (e.code === 'Space') {
        e.preventDefault();
        paused = !paused;
        const pauseBtn = document.getElementById('pause-btn');
        if (pauseBtn)
            pauseBtn.textContent = paused ? '▶' : '⏸';
    }
    if (e.code === 'ArrowRight' && paused) {
        // Step one frame
        const cw = window.innerWidth;
        const ch = window.innerHeight;
        (0, physics_1.physicsStep)(points, constraints, staticCircles, staticBoxes, cw, ch, C.gravity);
    }
});
// ─── Canvas Click Handler for Rope Spawning ────────────────────────
function setupCanvasEvents() {
    canvas.addEventListener('mousedown', (e) => {
        if (e.button === 0 && toolbar?.selectedTool === 'rope') {
            isSpawningRope = true;
            spawnStartX = input.mouseX;
            spawnStartY = input.mouseY;
        }
    });
    canvas.addEventListener('mouseup', (e) => {
        if (e.button === 0 && isSpawningRope) {
            isSpawningRope = false;
            // Spawn rope from start to end position
            const dx = input.mouseX - spawnStartX;
            const dy = input.mouseY - spawnStartY;
            const dist = Math.sqrt(dx * dx + dy * dy);
            if (dist > 20) {
                (0, spawner_1.spawnRope)(points, constraints, objects, spawnStartX, spawnStartY, input.mouseX, input.mouseY);
            }
            else {
                // Click without drag: spawn a short hanging rope
                (0, spawner_1.spawnRope)(points, constraints, objects, spawnStartX, spawnStartY, spawnStartX, spawnStartY + 150);
            }
        }
    });
}
// ─── Expose Simulation on Window for Probing ────────────────────────
const simObj = {
    get points() { return points; },
    get constraints() { return constraints; },
    get objects() { return objects; },
    step() {
        const cw = window.innerWidth;
        const ch = window.innerHeight;
        (0, physics_1.physicsStep)(points, constraints, staticCircles, staticBoxes, cw, ch, C.gravity);
    },
    reset: resetSim,
    get kineticEnergy() { return (0, physics_1.totalKineticEnergy)(points); },
};
// Assign to window for headless probing
window.sim = simObj;
// ─── Start ─────────────────────────────────────────────────────────
function start() {
    // Setup DOM
    document.getElementById('app').appendChild(canvas);
    // Toolbar
    toolbar = (0, ui_1.createToolbar)();
    (0, ui_1.setupToolbarEvents)(toolbar);
    document.getElementById('app').appendChild(toolbar.container);
    // Button handlers
    const pauseBtn = document.getElementById('pause-btn');
    if (pauseBtn) {
        pauseBtn.addEventListener('click', () => {
            paused = !paused;
            pauseBtn.textContent = paused ? '▶' : '⏸';
        });
    }
    const stepBtn = document.getElementById('step-btn');
    if (stepBtn) {
        stepBtn.addEventListener('click', () => {
            if (paused) {
                const cw = window.innerWidth;
                const ch = window.innerHeight;
                (0, physics_1.physicsStep)(points, constraints, staticCircles, staticBoxes, cw, ch, C.gravity);
            }
        });
    }
    const resetBtn = document.getElementById('reset-btn');
    if (resetBtn) {
        resetBtn.addEventListener('click', resetSim);
    }
    // Input setup
    (0, input_1.setupInput)(canvas, input);
    setupCanvasEvents();
    // Initialize simulation
    initSim();
    // Start game loop
    requestAnimationFrame(gameLoop);
}
// Auto-start when module loads
start();
};

// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONSTANTS = void 0;
// ─── Tunable Constants ──────────────────────────────────────────────
exports.CONSTANTS = {
    // Physics timestep (fixed) — 60 Hz
    dt: 1 / 60,
    // Damping per step (1.0 = no damping, < 1.0 = energy loss)
    damping: 0.955,
    // Constraint solver iterations per physics step
    iterations: 5,
    // Gravity strength (pixels/s²)
    gravity: 980,
    // Maximum frame delta to prevent explosion on tab-switch
    maxFrameDelta: 1 / 20,
    // Grab radius in pixels
    grabRadius: 15,
    // Tear/cut radius in pixels
    tearRadius: 12,
    // Point rendering radius
    pointRadius: 2.5,
    // Cloth point visibility (false = render as mesh)
    showPoints: false,
    // Stress visualization toggle
    stressVisualization: false,
    // Motion blur alpha (0 = no trail, higher = more trail)
    motionBlurAlpha: 0.15,
    // ─── Colors ──────────────────────────────────────────────────────
    colors: {
        background: '#1a1a2e',
        grid: 'rgba(255,255,255,0.03)',
        rope: '#ff6b6b',
        cloth: '#4ecdc4',
        softBody: '#ffe66d',
        ball: '#a8e6cf',
        pin: '#ffffff',
        grabHighlight: 'rgba(255, 107, 107, 0.3)',
        tearEffect: '#ff4757',
        wallStroke: 'rgba(255,255,255,0.15)',
        staticCircleFill: 'rgba(255,255,255,0.08)',
        staticCircleStroke: 'rgba(255,255,255,0.2)',
    },
    // ─── Object Defaults ─────────────────────────────────────────────
    ropeSegments: 30,
    ropeSpacing: 12,
    clothCols: 20,
    clothRows: 14,
    clothSpacing: 14,
    softBodyRadius: 50,
    softBodyPoints: 16,
    ballRadius: 20,
    // ─── Bounds ──────────────────────────────────────────────────────
    coordBound: 1e6, // NaN guard threshold
};
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ─── Core Types ─────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
};

// ── module: src/physics.ts ──
__mods["src/physics.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.integrate = integrate;
exports.solveConstraints = solveConstraints;
exports.collideWalls = collideWalls;
exports.collideStaticCircles = collideStaticCircles;
exports.collideStaticBoxes = collideStaticBoxes;
exports.physicsStep = physicsStep;
exports.totalKineticEnergy = totalKineticEnergy;
exports.constraintStretch = constraintStretch;
const constants_1 = require("./constants");
const C = constants_1.CONSTANTS;
// ─── NaN / Infinity Guard ──────────────────────────────────────────
function isFiniteCoord(v) {
    return Number.isFinite(v) && Math.abs(v) < C.coordBound;
}
function sanitizePoint(p) {
    if (!isFiniteCoord(p.x) || !isFiniteCoord(p.y)) {
        p.x = 0;
        p.y = 0;
        p.prevX = 0;
        p.prevY = 0;
    }
    if (!isFiniteCoord(p.prevX) || !isFiniteCoord(p.prevY)) {
        p.prevX = p.x;
        p.prevY = p.y;
    }
}
// ─── Verlet Integration Step ────────────────────────────────────────
function integrate(points, dt, gravity) {
    const damping = C.damping;
    for (let i = 0; i < points.length; i++) {
        const p = points[i];
        if (p.pinned)
            continue;
        // Verlet: x' = x + (x - prevX) * damping + a * dt²
        const vx = (p.x - p.prevX) * damping;
        const vy = (p.y - p.prevY) * damping;
        p.prevX = p.x;
        p.prevY = p.y;
        p.x += vx;
        p.y += vy + gravity * dt * dt;
        sanitizePoint(p);
    }
}
// ─── Distance Constraint Solver ─────────────────────────────────────
function solveConstraints(points, constraints) {
    const iterations = C.iterations;
    for (let iter = 0; iter < iterations; iter++) {
        for (let i = 0; i < constraints.length; i++) {
            const c = constraints[i];
            if (c.broken)
                continue;
            const pa = points[c.p0];
            const pb = points[c.p1];
            const dx = pb.x - pa.x;
            const dy = pb.y - pa.y;
            const distSq = dx * dx + dy * dy;
            // Guard against zero-length or NaN distances
            if (distSq < 1e-8 || !Number.isFinite(distSq))
                continue;
            const dist = Math.sqrt(distSq);
            const diff = (c.restLength - dist) / dist;
            // Weight by inverse mass (equal mass here, so split evenly unless pinned)
            const wa = pa.pinned ? 0 : 0.5;
            const wb = pb.pinned ? 0 : 0.5;
            const wSum = wa + wb;
            if (wSum < 1e-8)
                continue;
            const corrA = diff * wa / wSum;
            const corrB = diff * wb / wSum;
            pa.x -= dx * corrA;
            pa.y -= dy * corrA;
            pb.x += dx * corrB;
            pb.y += dy * corrB;
        }
    }
}
// ─── Wall/Floor Collision ──────────────────────────────────────────
function collideWalls(points, canvasW, canvasH) {
    const friction = 0.85;
    for (let i = 0; i < points.length; i++) {
        const p = points[i];
        if (p.pinned)
            continue;
        // Floor (account for point radius, minimum clearance of 2px)
        const floorOffset = Math.max(2, (p.radius || 0));
        if (p.y > canvasH - floorOffset) {
            p.y = canvasH - floorOffset;
            p.prevX = p.x + (p.prevX - p.x) * friction;
            p.prevY = p.y;
        }
        // Ceiling
        if (p.y < 2) {
            p.y = 2;
            p.prevX = p.x + (p.prevX - p.x) * friction;
            p.prevY = p.y;
        }
        // Left wall
        if (p.x < 2) {
            p.x = 2;
            p.prevX = p.x;
            p.prevY = p.y + (p.prevY - p.y) * friction;
        }
        // Right wall
        if (p.x > canvasW - 2) {
            p.x = canvasW - 2;
            p.prevX = p.x;
            p.prevY = p.y + (p.prevY - p.y) * friction;
        }
        sanitizePoint(p);
    }
}
// ─── Static Circle Collision ────────────────────────────────────────
function collideStaticCircles(points, circles) {
    const friction = 0.85;
    for (let ci = 0; ci < circles.length; ci++) {
        const circ = circles[ci];
        for (let pi = 0; pi < points.length; pi++) {
            const p = points[pi];
            if (p.pinned)
                continue;
            const dx = p.x - circ.x;
            const dy = p.y - circ.y;
            const distSq = dx * dx + dy * dy;
            const minDist = circ.radius + (p.radius || 0);
            if (distSq < minDist * minDist && distSq > 1e-8) {
                const dist = Math.sqrt(distSq);
                const nx = dx / dist;
                const ny = dy / dist;
                // Push point out of circle
                p.x = circ.x + nx * minDist;
                p.y = circ.y + ny * minDist;
                // Apply friction to tangential velocity
                const vx = p.x - p.prevX;
                const vy = p.y - p.prevY;
                const normalVel = vx * nx + vy * ny;
                if (normalVel < 0) {
                    p.prevX = p.x - (vx - normalVel * nx) * friction;
                    p.prevY = p.y - (vy - normalVel * ny) * friction;
                }
                else {
                    p.prevX = p.x - vx * friction;
                    p.prevY = p.y - vy * friction;
                }
                sanitizePoint(p);
            }
        }
    }
}
// ─── Static Box Collision ──────────────────────────────────────────
function collideStaticBoxes(points, boxes) {
    const friction = 0.85;
    for (let bi = 0; bi < boxes.length; bi++) {
        const box = boxes[bi];
        const cx = box.x + box.w / 2;
        const cy = box.y + box.h / 2;
        for (let pi = 0; pi < points.length; pi++) {
            const p = points[pi];
            if (p.pinned)
                continue;
            // Find closest point on AABB
            const closestX = Math.max(box.x, Math.min(p.x, box.x + box.w));
            const closestY = Math.max(box.y, Math.min(p.y, box.y + box.h));
            const dx = p.x - closestX;
            const dy = p.y - closestY;
            const distSq = dx * dx + dy * dy;
            const minDist = p.radius || 0;
            if (distSq < minDist * minDist && distSq > 1e-8) {
                const dist = Math.sqrt(distSq);
                let nx = dx / dist;
                let ny = dy / dist;
                // If point is inside the box, push out based on center direction
                if (dist < 0.5) {
                    const toCenterX = p.x - cx;
                    const toCenterY = p.y - cy;
                    const tcDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY);
                    if (tcDist > 1e-8) {
                        nx = toCenterX / tcDist;
                        ny = toCenterY / tcDist;
                    }
                    else {
                        nx = 0;
                        ny = -1;
                    }
                }
                p.x += nx * (minDist + 1);
                p.y += ny * (minDist + 1);
                const vx = p.x - p.prevX;
                const vy = p.y - p.prevY;
                const normalVel = vx * nx + vy * ny;
                if (normalVel < 0) {
                    p.prevX = p.x - (vx - normalVel * nx) * friction;
                    p.prevY = p.y - (vy - normalVel * ny) * friction;
                }
                else {
                    p.prevX = p.x - vx * friction;
                    p.prevY = p.y - vy * friction;
                }
                sanitizePoint(p);
            }
        }
    }
}
// ─── Full Physics Step ──────────────────────────────────────────────
function physicsStep(points, constraints, staticCircles, staticBoxes, canvasW, canvasH, gravity) {
    integrate(points, C.dt, gravity);
    for (let iter = 0; iter < C.iterations; iter++) {
        solveConstraints(points, constraints);
        collideWalls(points, canvasW, canvasH);
        collideStaticCircles(points, staticCircles);
        collideStaticBoxes(points, staticBoxes);
    }
    // Final NaN sweep
    for (let i = 0; i < points.length; i++) {
        sanitizePoint(points[i]);
    }
}
// ─── Kinetic Energy Calculator ──────────────────────────────────────
function totalKineticEnergy(points) {
    let energy = 0;
    for (let i = 0; i < points.length; i++) {
        const p = points[i];
        if (p.pinned)
            continue;
        const vx = p.x - p.prevX;
        const vy = p.y - p.prevY;
        energy += 0.5 * p.mass * (vx * vx + vy * vy);
    }
    return energy;
}
// ─── Constraint Stretch Ratio ──────────────────────────────────────
function constraintStretch(constraint, points) {
    const pa = points[constraint.p0];
    const pb = points[constraint.p1];
    const dx = pb.x - pa.x;
    const dy = pb.y - pa.y;
    const dist = Math.sqrt(dx * dx + dy * dy);
    if (dist < 1e-8 || !Number.isFinite(dist))
        return 0;
    return dist / constraint.restLength;
}
};

// ── module: src/spawner.ts ──
__mods["src/spawner.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.spawnRope = spawnRope;
exports.spawnCloth = spawnCloth;
exports.spawnSoftBody = spawnSoftBody;
exports.spawnBall = spawnBall;
exports.removeObject = removeObject;
const constants_1 = require("./constants");
const C = constants_1.CONSTANTS;
let nextId = 0;
function makePoint(x, y) {
    return { x, y, prevX: x, prevY: y, pinned: false, mass: 1 };
}
// ─── Rope Spawner ──────────────────────────────────────────────────
function spawnRope(points, constraints, objects, startX, startY, endX, endY) {
    const segments = C.ropeSegments;
    const spacing = C.ropeSpacing;
    // Calculate actual distance and adjust segment count if needed
    const dx = endX - startX;
    const dy = endY - startY;
    const totalLen = Math.sqrt(dx * dx + dy * dy);
    const segCount = Math.max(3, Math.min(segments, Math.floor(totalLen / spacing)));
    const pointStart = points.length;
    for (let i = 0; i <= segCount; i++) {
        const t = i / segCount;
        const px = startX + dx * t;
        const py = startY + dy * t;
        points.push(makePoint(px, py));
    }
    // Pin the first point
    points[pointStart].pinned = true;
    for (let i = 0; i < segCount; i++) {
        constraints.push({
            p0: pointStart + i,
            p1: pointStart + i + 1,
            restLength: spacing,
            broken: false,
        });
    }
    objects.push({
        id: `rope-${nextId++}`,
        type: 'rope',
        pointStart,
        constraintStart: constraints.length - segCount,
        pointCount: segCount + 1,
        constraintCount: segCount,
        color: C.colors.rope,
    });
}
// ─── Cloth Spawner ─────────────────────────────────────────────────
function spawnCloth(points, constraints, objects, x, y, pinPattern = 'corners') {
    const cols = C.clothCols;
    const rows = C.clothRows;
    const spacing = C.clothSpacing;
    const pointStart = points.length;
    // Create grid of points
    for (let row = 0; row < rows; row++) {
        for (let col = 0; col < cols; col++) {
            const px = x + col * spacing;
            const py = y + row * spacing;
            points.push(makePoint(px, py));
        }
    }
    // Pin top edge based on pattern
    if (pinPattern === 'fullEdge') {
        for (let col = 0; col < cols; col++) {
            points[pointStart + col].pinned = true;
        }
    }
    else {
        // Pin corners only
        points[pointStart].pinned = true;
        points[pointStart + cols - 1].pinned = true;
    }
    const constraintStart = constraints.length;
    // Structural constraints (horizontal + vertical)
    for (let row = 0; row < rows; row++) {
        for (let col = 0; col < cols; col++) {
            const idx = pointStart + row * cols + col;
            // Horizontal link
            if (col < cols - 1) {
                constraints.push({ p0: idx, p1: idx + 1, restLength: spacing, broken: false });
            }
            // Vertical link
            if (row < rows - 1) {
                constraints.push({ p0: idx, p1: idx + cols, restLength: spacing, broken: false });
            }
        }
    }
    // Shear constraints (diagonal) for stability
    for (let row = 0; row < rows - 1; row++) {
        for (let col = 0; col < cols - 1; col++) {
            const idx = pointStart + row * cols + col;
            // Diagonal constraints
            constraints.push({ p0: idx, p1: idx + cols + 1, restLength: spacing * Math.SQRT2, broken: false });
        }
    }
    objects.push({
        id: `cloth-${nextId++}`,
        type: 'cloth',
        pointStart,
        constraintStart,
        pointCount: rows * cols,
        constraintCount: constraints.length - constraintStart,
        color: C.colors.cloth,
    });
}
// ─── Soft Body Spawner ─────────────────────────────────────────────
function spawnSoftBody(points, constraints, objects, cx, cy, radius) {
    const r = radius ?? C.softBodyRadius;
    const nPts = C.softBodyPoints;
    const pointStart = points.length;
    // Create circular arrangement of points
    for (let i = 0; i < nPts; i++) {
        const angle = (2 * Math.PI * i) / nPts - Math.PI / 2;
        const px = cx + r * Math.cos(angle);
        const py = cy + r * Math.sin(angle);
        points.push(makePoint(px, py));
    }
    const constraintStart = constraints.length;
    // Edge constraints (perimeter)
    for (let i = 0; i < nPts; i++) {
        const next = (i + 1) % nPts;
        const p0 = points[pointStart + i];
        const p1 = points[pointStart + next];
        const dx = p1.x - p0.x;
        const dy = p1.y - p0.y;
        constraints.push({
            p0: pointStart + i,
            p1: pointStart + next,
            restLength: Math.sqrt(dx * dx + dy * dy),
            broken: false,
        });
    }
    // Internal brace constraints (skip-2 connections for shape retention)
    for (let i = 0; i < nPts; i++) {
        const skip2 = (i + 3) % nPts;
        const p0 = points[pointStart + i];
        const p1 = points[pointStart + skip2];
        const dx = p1.x - p0.x;
        const dy = p1.y - p0.y;
        constraints.push({
            p0: pointStart + i,
            p1: pointStart + skip2,
            restLength: Math.sqrt(dx * dx + dy * dy),
            broken: false,
        });
    }
    objects.push({
        id: `softbody-${nextId++}`,
        type: 'softBody',
        pointStart,
        constraintStart,
        pointCount: nPts, // no center point
        constraintCount: constraints.length - constraintStart,
        color: C.colors.softBody,
    });
}
// ─── Ball Spawner ──────────────────────────────────────────────────
function spawnBall(points, constraints, objects, x, y, radius) {
    const r = radius ?? C.ballRadius;
    const pointStart = points.length;
    const p = makePoint(x, y);
    p.radius = r;
    points.push(p);
    objects.push({
        id: `ball-${nextId++}`,
        type: 'ball',
        pointStart,
        constraintStart: constraints.length,
        pointCount: 1,
        constraintCount: 0,
        color: C.colors.ball,
    });
}
// ─── Remove Object ─────────────────────────────────────────────────
function removeObject(points, constraints, objects, objId) {
    const idx = objects.findIndex(o => o.id === objId);
    if (idx < 0)
        return;
    // Mark constraints as broken instead of removing to avoid index issues
    for (let i = 0; i < objects[idx].constraintCount; i++) {
        constraints[objects[idx].constraintStart + i].broken = true;
    }
    // Remove points by setting them off-screen and pinned
    for (let i = 0; i < objects[idx].pointCount; i++) {
        const p = points[objects[idx].pointStart + i];
        p.x = -1e6;
        p.y = -1e6;
        p.prevX = -1e6;
        p.prevY = -1e6;
        p.pinned = true;
    }
    objects.splice(idx, 1);
}
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupCanvas = setupCanvas;
exports.render = render;
exports.resizeCanvas = resizeCanvas;
const constants_1 = require("./constants");
const physics_1 = require("./physics");
const C = constants_1.CONSTANTS;
// ─── Canvas Setup ──────────────────────────────────────────────────
function setupCanvas() {
    const canvas = document.createElement('canvas');
    canvas.id = 'sim-canvas';
    canvas.style.display = 'block';
    return canvas;
}
function resizeCanvas(canvas) {
    const dpr = window.devicePixelRatio || 1;
    const w = window.innerWidth;
    const h = window.innerHeight;
    canvas.width = w * dpr;
    canvas.height = h * dpr;
    canvas.style.width = w + 'px';
    canvas.style.height = h + 'px';
}
// ─── Stress Color Helper ────────────────────────────────────────────
function stressColor(ratio) {
    // Green (1.0) → Yellow (1.2) → Red (1.5+)
    const t = Math.min(1, Math.max(0, (ratio - 1) / 0.5));
    const r = Math.floor(t * 255);
    const g = Math.floor((1 - t) * 255);
    return `rgb(${r},${g},64)`;
}
// ─── Main Render Function ──────────────────────────────────────────
function render(ctx, points, constraints, objects, staticCircles, staticBoxes, canvasW, canvasH, grabPointIdx, fps, stepTimeMs, paused) {
    const dpr = window.devicePixelRatio || 1;
    // Motion blur trail
    ctx.fillStyle = `rgba(${parseInt(C.colors.background.slice(1, 3), 16)},${parseInt(C.colors.background.slice(3, 5), 16)},${parseInt(C.colors.background.slice(5, 7), 16)},${C.motionBlurAlpha})`;
    ctx.fillRect(0, 0, canvasW * dpr, canvasH * dpr);
    // Clear fully on first frame or when not using motion blur
    if (C.motionBlurAlpha < 0.01) {
        ctx.fillStyle = C.colors.background;
        ctx.fillRect(0, 0, canvasW * dpr, canvasH * dpr);
    }
    ctx.save();
    ctx.scale(dpr, dpr);
    // ─── Draw Walls ──────────────────────────────────────────────────
    drawWalls(ctx, canvasW, canvasH);
    // ─── Draw Static Geometry ────────────────────────────────────────
    for (const circ of staticCircles) {
        ctx.beginPath();
        ctx.arc(circ.x, circ.y, circ.radius, 0, Math.PI * 2);
        ctx.fillStyle = C.colors.staticCircleFill;
        ctx.fill();
        ctx.strokeStyle = C.colors.staticCircleStroke;
        ctx.lineWidth = 1.5;
        ctx.stroke();
    }
    for (const box of staticBoxes) {
        ctx.fillStyle = C.colors.staticCircleFill;
        ctx.fillRect(box.x, box.y, box.w, box.h);
        ctx.strokeStyle = C.colors.staticCircleStroke;
        ctx.lineWidth = 1.5;
        ctx.strokeRect(box.x, box.y, box.w, box.h);
    }
    // ─── Draw Objects ────────────────────────────────────────────────
    for (const obj of objects) {
        switch (obj.type) {
            case 'rope':
                drawRope(ctx, points, constraints, obj);
                break;
            case 'cloth':
                drawCloth(ctx, points, constraints, obj);
                break;
            case 'softBody':
                drawSoftBody(ctx, points, constraints, obj);
                break;
            case 'ball':
                drawBall(ctx, points, obj);
                break;
        }
    }
    // ─── Draw Grab Highlight ─────────────────────────────────────────
    if (grabPointIdx !== null && grabPointIdx < points.length) {
        const gp = points[grabPointIdx];
        ctx.beginPath();
        ctx.arc(gp.x, gp.y, C.grabRadius + 5, 0, Math.PI * 2);
        ctx.fillStyle = C.colors.grabHighlight;
        ctx.fill();
    }
    // ─── Draw Pins ───────────────────────────────────────────────────
    for (const obj of objects) {
        for (let i = 0; i < obj.pointCount; i++) {
            const p = points[obj.pointStart + i];
            if (!p.pinned)
                continue;
            ctx.beginPath();
            ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
            ctx.fillStyle = C.colors.pin;
            ctx.fill();
            // Crosshair
            ctx.strokeStyle = C.colors.pin;
            ctx.lineWidth = 1.5;
            ctx.beginPath();
            ctx.moveTo(p.x - 6, p.y);
            ctx.lineTo(p.x + 6, p.y);
            ctx.moveTo(p.x, p.y - 6);
            ctx.lineTo(p.x, p.y + 6);
            ctx.stroke();
        }
    }
    // ─── HUD ─────────────────────────────────────────────────────────
    drawHUD(ctx, canvasW, fps, stepTimeMs, paused);
    ctx.restore();
}
// ─── Rope Drawing ──────────────────────────────────────────────────
function drawRope(ctx, points, constraints, obj) {
    const showStress = C.stressVisualization;
    for (let i = 0; i < obj.constraintCount; i++) {
        const c = constraints[obj.constraintStart + i];
        if (c.broken)
            continue;
        ctx.beginPath();
        ctx.moveTo(points[c.p0].x, points[c.p0].y);
        ctx.lineTo(points[c.p1].x, points[c.p1].y);
        if (showStress) {
            const stretch = (0, physics_1.constraintStretch)(c, points);
            ctx.strokeStyle = stressColor(stretch);
        }
        else {
            ctx.strokeStyle = obj.color;
        }
        ctx.lineWidth = 2.5;
        ctx.lineCap = 'round';
        ctx.stroke();
    }
    // Draw endpoints as small dots
    for (let i = 0; i < obj.pointCount; i++) {
        const p = points[obj.pointStart + i];
        ctx.beginPath();
        ctx.arc(p.x, p.y, C.pointRadius, 0, Math.PI * 2);
        ctx.fillStyle = obj.color;
        ctx.fill();
    }
}
// ─── Cloth Drawing ─────────────────────────────────────────────────
function drawCloth(ctx, points, constraints, obj) {
    const cols = C.clothCols;
    const rows = C.clothRows;
    // Draw filled mesh (quads)
    for (let row = 0; row < rows - 1; row++) {
        for (let col = 0; col < cols - 1; col++) {
            const i00 = obj.pointStart + row * cols + col;
            const i10 = i00 + 1;
            const i01 = i00 + cols;
            const i11 = i00 + cols + 1;
            const p00 = points[i00];
            const p10 = points[i10];
            const p01 = points[i01];
            const p11 = points[i11];
            // Check if any constraint in this quad is broken (for tear visualization)
            let torn = false;
            for (let ci = 0; ci < obj.constraintCount && !torn; ci++) {
                const c = constraints[obj.constraintStart + ci];
                if (!c.broken)
                    continue;
                const cpSet = new Set([c.p0, c.p1]);
                if ([i00, i10, i01, i11].some(idx => cpSet.has(idx))) {
                    torn = true;
                }
            }
            ctx.beginPath();
            ctx.moveTo(p00.x, p00.y);
            ctx.lineTo(p10.x, p10.y);
            ctx.lineTo(p11.x, p11.y);
            ctx.lineTo(p01.x, p01.y);
            ctx.closePath();
            if (torn) {
                ctx.fillStyle = 'rgba(255, 71, 87, 0.3)';
            }
            else {
                ctx.fillStyle = obj.color + '40'; // ~25% alpha
            }
            ctx.fill();
            // Grid lines
            ctx.strokeStyle = obj.color + '80'; // ~50% alpha
            ctx.lineWidth = 0.7;
            ctx.stroke();
        }
    }
    // Draw points if enabled
    if (C.showPoints) {
        for (let i = 0; i < obj.pointCount; i++) {
            const p = points[obj.pointStart + i];
            ctx.beginPath();
            ctx.arc(p.x, p.y, C.pointRadius, 0, Math.PI * 2);
            ctx.fillStyle = obj.color;
            ctx.fill();
        }
    }
}
// ─── Soft Body Drawing ─────────────────────────────────────────────
function drawSoftBody(ctx, points, constraints, obj) {
    const nPts = C.softBodyPoints;
    // Draw filled perimeter polygon
    ctx.beginPath();
    for (let i = 0; i < nPts; i++) {
        const p = points[obj.pointStart + i];
        if (i === 0)
            ctx.moveTo(p.x, p.y);
        else
            ctx.lineTo(p.x, p.y);
    }
    ctx.closePath();
    ctx.fillStyle = obj.color + '50'; // ~31% alpha
    ctx.fill();
    // Draw edge constraints
    for (let i = 0; i < nPts; i++) {
        const next = (i + 1) % nPts;
        const p0 = points[obj.pointStart + i];
        const p1 = points[obj.pointStart + next];
        ctx.beginPath();
        ctx.moveTo(p0.x, p0.y);
        ctx.lineTo(p1.x, p1.y);
        ctx.strokeStyle = obj.color;
        ctx.lineWidth = 2;
        ctx.lineCap = 'round';
        ctx.stroke();
    }
    // Draw internal constraints (faint) — skip-2 bracing
    for (let i = 0; i < nPts; i++) {
        const skip2 = (i + 3) % nPts;
        const p0 = points[obj.pointStart + i];
        const p1 = points[obj.pointStart + skip2];
        ctx.beginPath();
        ctx.moveTo(p0.x, p0.y);
        ctx.lineTo(p1.x, p1.y);
        ctx.strokeStyle = obj.color + '25';
        ctx.lineWidth = 0.5;
        ctx.stroke();
    }
    // Draw points
    for (let i = 0; i < nPts; i++) {
        const p = points[obj.pointStart + i];
        ctx.beginPath();
        ctx.arc(p.x, p.y, C.pointRadius, 0, Math.PI * 2);
        ctx.fillStyle = obj.color;
        ctx.fill();
    }
}
// ─── Ball Drawing ──────────────────────────────────────────────────
function drawBall(ctx, points, obj) {
    const p = points[obj.pointStart];
    const r = p.radius || C.ballRadius;
    // Glow effect
    const gradient = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, r * 1.5);
    gradient.addColorStop(0, obj.color + 'cc');
    gradient.addColorStop(0.7, obj.color + '44');
    gradient.addColorStop(1, obj.color + '00');
    ctx.beginPath();
    ctx.arc(p.x, p.y, r * 1.5, 0, Math.PI * 2);
    ctx.fillStyle = gradient;
    ctx.fill();
    // Solid ball
    ctx.beginPath();
    ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
    ctx.fillStyle = obj.color;
    ctx.fill();
    ctx.strokeStyle = obj.color + 'aa';
    ctx.lineWidth = 1.5;
    ctx.stroke();
}
// ─── Wall Drawing ──────────────────────────────────────────────────
function drawWalls(ctx, w, h) {
    ctx.strokeStyle = C.colors.wallStroke;
    ctx.lineWidth = 3;
    // Floor
    ctx.beginPath();
    ctx.moveTo(0, h - 1);
    ctx.lineTo(w, h - 1);
    ctx.stroke();
    // Walls (subtle)
    ctx.lineWidth = 2;
    ctx.globalAlpha = 0.5;
    ctx.beginPath();
    ctx.moveTo(1, 0);
    ctx.lineTo(1, h);
    ctx.moveTo(w - 1, 0);
    ctx.lineTo(w - 1, h);
    ctx.stroke();
    ctx.globalAlpha = 1;
}
// ─── HUD Drawing ───────────────────────────────────────────────────
function drawHUD(ctx, w, fps, stepTimeMs, paused) {
    ctx.font = '12px monospace';
    ctx.fillStyle = 'rgba(255,255,255,0.6)';
    const lines = [
        `FPS: ${fps}`,
        `Physics step: ${stepTimeMs.toFixed(1)}ms`,
        paused ? '⏸ PAUSED' : '',
    ].filter(Boolean);
    for (let i = 0; i < lines.length; i++) {
        ctx.fillText(lines[i], 12, 20 + i * 18);
    }
}
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInputState = createInputState;
exports.findNearestPoint = findNearestPoint;
exports.findNearestConstraint = findNearestConstraint;
exports.setupInput = setupInput;
exports.processInput = processInput;
const constants_1 = require("./constants");
const C = constants_1.CONSTANTS;
function createInputState() {
    return {
        mouseX: 0, mouseY: 0,
        mouseDown: false, rightMouseDown: false,
        shiftKey: false,
        grabPointIdx: null,
        dragVelX: 0, dragVelY: 0,
        lastMouseX: 0, lastMouseY: 0,
    };
}
// ─── Find Nearest Point ─────────────────────────────────────────────
function findNearestPoint(points, objects, x, y, radius) {
    let bestIdx = -1;
    let bestDistSq = radius * radius;
    for (const obj of objects) {
        for (let i = 0; i < obj.pointCount; i++) {
            const p = points[obj.pointStart + i];
            if (p.pinned)
                continue; // Don't grab pinned points
            const dx = p.x - x;
            const dy = p.y - y;
            const distSq = dx * dx + dy * dy;
            if (distSq < bestDistSq) {
                bestDistSq = distSq;
                bestIdx = obj.pointStart + i;
            }
        }
    }
    return bestIdx >= 0 ? bestIdx : null;
}
// ─── Find Nearest Constraint for Tearing ────────────────────────────
function findNearestConstraint(constraints, points, x, y, radius) {
    let bestIdx = -1;
    let bestDistSq = radius * radius;
    for (let i = 0; i < constraints.length; i++) {
        const c = constraints[i];
        if (c.broken)
            continue;
        // Distance from point to line segment
        const pa = points[c.p0];
        const pb = points[c.p1];
        const dx = pb.x - pa.x;
        const dy = pb.y - pa.y;
        const lenSq = dx * dx + dy * dy;
        if (lenSq < 1e-8)
            continue;
        // Project point onto line segment
        let t = ((x - pa.x) * dx + (y - pa.y) * dy) / lenSq;
        t = Math.max(0, Math.min(1, t));
        const closestX = pa.x + t * dx;
        const closestY = pa.y + t * dy;
        const distDx = x - closestX;
        const distDy = y - closestY;
        const distSq = distDx * distDx + distDy * distDy;
        if (distSq < bestDistSq) {
            bestDistSq = distSq;
            bestIdx = i;
        }
    }
    return bestIdx >= 0 ? bestIdx : null;
}
// ─── Setup Event Listeners ──────────────────────────────────────────
function setupInput(canvas, input) {
    const dpr = () => window.devicePixelRatio || 1;
    // Mouse move
    canvas.addEventListener('mousemove', (e) => {
        const rect = canvas.getBoundingClientRect();
        input.lastMouseX = input.mouseX;
        input.lastMouseY = input.mouseY;
        input.mouseX = e.clientX - rect.left;
        input.mouseY = e.clientY - rect.top;
        // Track drag velocity for throw
        if (input.mouseDown && input.grabPointIdx !== null) {
            input.dragVelX = input.mouseX - input.lastMouseX;
            input.dragVelY = input.mouseY - input.lastMouseY;
        }
    });
    // Mouse down (left click)
    canvas.addEventListener('mousedown', (e) => {
        if (e.button === 0) {
            input.mouseDown = true;
        }
        else if (e.button === 2) {
            input.rightMouseDown = true;
        }
    });
    // Mouse up
    window.addEventListener('mouseup', (e) => {
        if (e.button === 0) {
            input.mouseDown = false;
            input.grabPointIdx = null;
        }
        else if (e.button === 2) {
            input.rightMouseDown = false;
        }
    });
    // Prevent context menu on right click
    canvas.addEventListener('contextmenu', (e) => e.preventDefault());
    // Keyboard modifiers
    window.addEventListener('keydown', (e) => {
        if (e.key === 'Shift')
            input.shiftKey = true;
    });
    window.addEventListener('keyup', (e) => {
        if (e.key === 'Shift')
            input.shiftKey = false;
    });
    // Touch support
    canvas.addEventListener('touchstart', (e) => {
        e.preventDefault();
        const touch = e.touches[0];
        const rect = canvas.getBoundingClientRect();
        input.mouseX = touch.clientX - rect.left;
        input.mouseY = touch.clientY - rect.top;
        input.lastMouseX = input.mouseX;
        input.lastMouseY = input.mouseY;
        input.mouseDown = true;
    }, { passive: false });
    canvas.addEventListener('touchmove', (e) => {
        e.preventDefault();
        const touch = e.touches[0];
        const rect = canvas.getBoundingClientRect();
        input.lastMouseX = input.mouseX;
        input.lastMouseY = input.mouseY;
        input.mouseX = touch.clientX - rect.left;
        input.mouseY = touch.clientY - rect.top;
        if (input.grabPointIdx !== null) {
            input.dragVelX = input.mouseX - input.lastMouseX;
            input.dragVelY = input.mouseY - input.lastMouseY;
        }
    }, { passive: false });
    canvas.addEventListener('touchend', () => {
        input.mouseDown = false;
        input.grabPointIdx = null;
    });
}
// ─── Process Input Each Frame ──────────────────────────────────────
function processInput(input, points, constraints) {
    // Grab logic (left click)
    if (input.mouseDown && input.grabPointIdx === null) {
        const idx = findNearestPoint(points, [], input.mouseX, input.mouseY, C.grabRadius);
        // We need to pass objects but we'll handle it in main loop instead
        // For now, just store the position for later use
    }
    // Tear logic (right click or shift+drag)
    if (input.rightMouseDown || (input.shiftKey && input.mouseDown)) {
        const idx = findNearestConstraint(constraints, points, input.mouseX, input.mouseY, C.tearRadius);
        if (idx !== null) {
            constraints[idx].broken = true;
        }
    }
    // Apply grab position to point
    if (input.grabPointIdx !== null && input.grabPointIdx < points.length) {
        const p = points[input.grabPointIdx];
        p.x = input.mouseX;
        p.y = input.mouseY;
        p.prevX = input.mouseX - input.dragVelX * 3; // Throw velocity multiplier
        p.prevY = input.mouseY - input.dragVelY * 3;
    }
}
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
// ─── Toolbar & Controls UI ──────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.createToolbar = createToolbar;
exports.setupToolbarEvents = setupToolbarEvents;
const TOOL_DEFS = [
    { id: 'rope', label: '🪢 Rope', title: 'Click-drag to draw a rope' },
    { id: 'cloth', label: '🧵 Cloth', title: 'Click to place cloth' },
    { id: 'softbody', label: '🫧 Soft Body', title: 'Click to spawn soft body' },
    { id: 'ball', label: '⚽ Ball', title: 'Click to drop a ball' },
];
function createToolbar() {
    const container = document.createElement('div');
    container.id = 'toolbar';
    // Tool buttons
    for (const tool of TOOL_DEFS) {
        const btn = document.createElement('button');
        btn.className = 'tool-btn';
        btn.dataset.tool = tool.id;
        btn.textContent = tool.label;
        btn.title = tool.title;
        container.appendChild(btn);
    }
    // Separator
    const sep = document.createElement('div');
    sep.className = 'toolbar-sep';
    container.appendChild(sep);
    // Gravity slider
    const gravLabel = document.createElement('label');
    gravLabel.textContent = 'Gravity';
    gravLabel.title = 'Toggle and adjust gravity strength';
    container.appendChild(gravLabel);
    const gravSlider = document.createElement('input');
    gravSlider.type = 'range';
    gravSlider.min = '0';
    gravSlider.max = '2000';
    gravSlider.value = '980';
    gravSlider.step = '10';
    gravSlider.className = 'slider';
    container.appendChild(gravSlider);
    // Iterations slider
    const iterLabel = document.createElement('label');
    iterLabel.textContent = 'Iterations';
    iterLabel.title = 'Constraint solver iterations (higher = more stable)';
    container.appendChild(iterLabel);
    const iterSlider = document.createElement('input');
    iterSlider.type = 'range';
    iterSlider.min = '1';
    iterSlider.max = '12';
    iterSlider.value = '5';
    iterSlider.step = '1';
    iterSlider.className = 'slider';
    container.appendChild(iterSlider);
    // Stress toggle
    const stressLabel = document.createElement('label');
    stressLabel.textContent = 'Stress';
    stressLabel.title = 'Color constraints by stretch ratio';
    container.appendChild(stressLabel);
    const stressToggle = document.createElement('input');
    stressToggle.type = 'checkbox';
    stressToggle.className = 'toggle';
    container.appendChild(stressToggle);
    // Pause button
    const pauseBtn = document.createElement('button');
    pauseBtn.className = 'tool-btn';
    pauseBtn.id = 'pause-btn';
    pauseBtn.textContent = '⏸';
    pauseBtn.title = 'Pause (Space)';
    container.appendChild(pauseBtn);
    // Step button
    const stepBtn = document.createElement('button');
    stepBtn.className = 'tool-btn';
    stepBtn.id = 'step-btn';
    stepBtn.textContent = '⏭';
    stepBtn.title = 'Step one frame (when paused)';
    container.appendChild(stepBtn);
    // Reset button
    const resetBtn = document.createElement('button');
    resetBtn.className = 'tool-btn';
    resetBtn.id = 'reset-btn';
    resetBtn.textContent = '🔄';
    resetBtn.title = 'Reset simulation';
    container.appendChild(resetBtn);
    return { container, selectedTool: 'rope', gravitySlider: gravSlider, iterationsSlider: iterSlider, stressToggle };
}
function setupToolbarEvents(toolbar) {
    // Tool selection
    toolbar.container.addEventListener('click', (e) => {
        const btn = e.target.closest('.tool-btn');
        if (!btn || !btn.dataset.tool)
            return;
        // Deselect all
        toolbar.container.querySelectorAll('.tool-btn[data-tool]').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        toolbar.selectedTool = btn.dataset.tool;
    });
    // Select first tool by default
    const firstBtn = toolbar.container.querySelector('.tool-btn[data-tool]');
    if (firstBtn)
        firstBtn.classList.add('active');
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"8ef7bf8f-6408-4c6c-bc7b-462c1c8a9b32","tokensIn":2215468,"tokensOut":35367,"tokensTotal":2250835,"cost":0,"turns":67,"toolCalls":68,"failedToolCalls":0,"timestamp":"2026-07-31T20:19:48.800Z"} -->