← Physics Playground results

Physics Playground

Qwen 3.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 66,466 bytes · SHA-256 5b6a36162a2d
<!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: 'Segoe UI', system-ui, sans-serif;
}

#app {
  display: flex;
  flex-direction: column;
  height: 100vh;
  overflow: hidden;
}

#toolbar {
  flex-shrink: 0;
  z-index: 10;
}

canvas {
  display: block;
  cursor: crosshair;
}

canvas:active {
  cursor: grabbing;
}
</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","./input":"src/input.ts","./renderer":"src/renderer.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/input.ts":{"./constants":"src/constants.ts","./types":"src/types.ts","./physics":"src/physics.ts"},"src/renderer.ts":{"./constants":"src/constants.ts","./types":"src/types.ts","./physics":"src/physics.ts","./spawner":"src/spawner.ts"},"src/ui.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 input_1 = require("./input");
const renderer_1 = require("./renderer");
const ui_1 = require("./ui");
// ===== Simulation State =====
let points = [];
let constraints = [];
let objects = [];
let staticShapes = [];
let nextPointId = 0;
let nextObjId = 0;
// Physics accumulator
let accumulator = 0;
let lastTime = 0;
let paused = false;
let stepOnce = false;
let currentGravity = constants_1.PHYSICS.gravity;
let currentIterations = constants_1.PHYSICS.iterations;
let showStress = false;
let clothPinPattern = 'corners';
let windForce = 0;
// Rendering
let canvas;
let renderer;
let inputHandler;
let toolbar;
// FPS tracking
let fps = 0;
let frameCount = 0;
let fpsTime = 0;
let physicsTime = 0;
// Current spawn tool
let currentTool = 'select';
// Spawn tracking
let pendingSpawn = false;
let spawnStartX = 0;
let spawnStartY = 0;
// Expose sim on window for probing
const sim = {
    get points() { return points; },
    get constraints() { return constraints; },
    get objects() { return objects; },
    get staticShapes() { return staticShapes; },
    step: () => {
        physicsStep(constants_1.PHYSICS.timestep);
    },
    get kineticEnergy() {
        return (0, physics_1.kineticEnergy)(points, constants_1.PHYSICS.timestep);
    },
    get gravity() { return currentGravity; },
    set gravity(v) { currentGravity = v; },
    get iterations() { return currentIterations; },
    set iterations(v) { currentIterations = v; },
    get paused() { return paused; },
    set paused(v) { paused = v; },
};
// ===== Initialization =====
function init() {
    // Setup DOM
    const app = document.getElementById('app');
    app.style.cssText = 'display:flex;flex-direction:column;height:100vh;overflow:hidden;background:' + constants_1.COLORS.bg;
    // Toolbar container
    const toolbarDiv = document.createElement('div');
    toolbarDiv.id = 'toolbar';
    app.appendChild(toolbarDiv);
    // Canvas container
    const canvasDiv = document.createElement('div');
    canvasDiv.style.cssText = 'flex:1;position:relative;overflow:hidden';
    app.appendChild(canvasDiv);
    // Canvas
    canvas = document.createElement('canvas');
    canvas.style.cssText = 'display:block;width:100%;height:100%';
    canvasDiv.appendChild(canvas);
    // Renderer
    renderer = new renderer_1.Renderer(canvas);
    // Resize
    const resize = () => {
        const rect = canvasDiv.getBoundingClientRect();
        renderer.resize(rect.width, rect.height);
    };
    window.addEventListener('resize', resize);
    resize();
    // Input
    inputHandler = new input_1.InputHandler(canvas);
    // Toolbar
    toolbar = new ui_1.Toolbar(toolbarDiv);
    toolbar.setOnToolChange((tool) => { currentTool = tool; });
    toolbar.setOnReset(() => resetSim());
    toolbar.setOnGravityChange((g) => { currentGravity = g; });
    toolbar.setOnIterChange((n) => { currentIterations = n; });
    toolbar.setOnPinPatternChange((p) => { clothPinPattern = p; });
    toolbar.setOnStressToggle(() => { showStress = !showStress; });
    toolbar.setOnWindChange((w) => { windForce = w; });
    toolbar.setOnPointsToggle(() => { constants_1.RENDER.showPoints = !constants_1.RENDER.showPoints; });
    // Init static shapes
    initStaticShapes();
    // Spawn demo objects
    spawnDemoObjects();
    // Start loop
    lastTime = performance.now();
    requestAnimationFrame(gameLoop);
}
function initStaticShapes() {
    const w = canvas.width;
    const h = canvas.height;
    // A big circle obstacle in the center
    staticShapes.push({ x: w * 0.5, y: h * 0.55, radius: 50 });
    // A box obstacle on the left
    staticShapes.push({ x: w * 0.2, y: h * 0.7, w: 100, h: 20 });
    // Another box on the right
    staticShapes.push({ x: w * 0.72, y: h * 0.65, w: 80, h: 15 });
}
function spawnDemoObjects() {
    const w = canvas.width;
    const h = canvas.height;
    // Spawn a rope (pendulum)
    {
        const r = (0, spawner_1.spawnRope)(points, constraints, objects, w * 0.15, 30, w * 0.15, h * 0.45, 20, nextPointId, nextObjId);
        nextPointId = r.nextPointId;
        nextObjId = r.nextObjId;
    }
    // Spawn a cloth
    {
        const r = (0, spawner_1.spawnCloth)(points, constraints, objects, w * 0.42, 30, 15, 12, 12, clothPinPattern, nextPointId, nextObjId);
        nextPointId = r.nextPointId;
        nextObjId = r.nextObjId;
    }
    // Spawn a soft body
    {
        const r = (0, spawner_1.spawnSoftBody)(points, constraints, objects, w * 0.82, h * 0.25, 40, 8, 3, nextPointId, nextObjId);
        nextPointId = r.nextPointId;
        nextObjId = r.nextObjId;
    }
    // Spawn a ball
    {
        const r = (0, spawner_1.spawnBall)(points, constraints, objects, w * 0.5, h * 0.12, 15, nextPointId, nextObjId);
        nextPointId = r.nextPointId;
        nextObjId = r.nextObjId;
    }
}
function resetSim() {
    points = [];
    constraints = [];
    objects = [];
    nextPointId = 0;
    nextObjId = 0;
    accumulator = 0;
    staticShapes = [];
    initStaticShapes();
    spawnDemoObjects();
}
// ===== Physics Step =====
function physicsStep(dt) {
    // Apply wind force
    if (windForce !== 0) {
        for (let i = 0; i < points.length; i++) {
            const p = points[i];
            if (p.pinned)
                continue;
            p.x += windForce * dt * dt;
        }
    }
    // Integrate
    (0, physics_1.integrate)(points, dt, currentGravity, constants_1.PHYSICS.damping);
    // Solve constraints
    (0, physics_1.solveConstraints)(points, constraints, currentIterations);
    // Collide with static shapes
    (0, physics_1.collideStatic)(points, staticShapes);
    // Collide with bounds
    (0, physics_1.collideBounds)(points, canvas.width, canvas.height);
    // NaN guard - reset any NaN points
    if ((0, physics_1.checkNaN)(points)) {
        for (let i = 0; i < points.length; i++) {
            const p = points[i];
            if (!isFinite(p.x) || !isFinite(p.y)) {
                p.x = p.pinned ? p.x : canvas.width / 2;
                p.y = p.pinned ? p.y : 100;
                p.prevX = p.x;
                p.prevY = p.y;
            }
            if (!isFinite(p.prevX) || !isFinite(p.prevY)) {
                p.prevX = p.x;
                p.prevY = p.y;
            }
        }
    }
}
// ===== Game Loop =====
function gameLoop(timestamp) {
    // FPS tracking
    frameCount++;
    fpsTime += timestamp - (lastTime || timestamp);
    if (fpsTime >= 1000) {
        fps = frameCount;
        frameCount = 0;
        fpsTime = 0;
    }
    // Delta time with clamp
    let dt = (timestamp - lastTime) / 1000;
    lastTime = timestamp;
    dt = Math.min(dt, constants_1.PHYSICS.maxDt); // clamp to prevent explosion on tab switch
    // Handle pause/step
    if (paused && !stepOnce) {
        render();
        requestAnimationFrame(gameLoop);
        return;
    }
    if (stepOnce) {
        physicsStep(constants_1.PHYSICS.timestep);
        stepOnce = false;
        render();
        requestAnimationFrame(gameLoop);
        return;
    }
    // Fixed timestep accumulator
    accumulator += dt;
    let steps = 0;
    const maxSteps = 10; // prevent spiral of death
    const stepStart = performance.now();
    while (accumulator >= constants_1.PHYSICS.timestep && steps < maxSteps) {
        physicsStep(constants_1.PHYSICS.timestep);
        accumulator -= constants_1.PHYSICS.timestep;
        steps++;
    }
    // Drain excess accumulator to prevent spiral of death
    if (accumulator > constants_1.PHYSICS.timestep * 5) {
        accumulator = 0;
    }
    physicsTime = performance.now() - stepStart;
    // Process input
    const inputResult = inputHandler.process(points, constraints, canvas.width, canvas.height);
    // Handle grab release (throw)
    if (inputResult.releaseGrab && inputResult.grabPoint !== null) {
        inputHandler.applyThrow(points, inputResult.grabPoint);
    }
    // Handle tear
    for (const ci of inputResult.tearConstraints) {
        constraints[ci].active = false;
    }
    // Handle spawn on release
    if (currentTool !== 'select' && !inputHandler.state.mouseDown && pendingSpawn) {
        const dragLine = inputHandler.getDragLine();
        if (dragLine) {
            spawnObject(dragLine.startX, dragLine.startY, dragLine.endX, dragLine.endY);
        }
        pendingSpawn = false;
    }
    // Track pending spawn
    if (currentTool !== 'select' && inputHandler.state.mouseDown) {
        if (!pendingSpawn) {
            pendingSpawn = true;
            spawnStartX = inputHandler.state.mouseX;
            spawnStartY = inputHandler.state.mouseY;
        }
    }
    // Render
    render();
    requestAnimationFrame(gameLoop);
}
function spawnObject(startX, startY, endX, endY) {
    const w = canvas.width;
    const h = canvas.height;
    switch (currentTool) {
        case 'rope': {
            const dx = endX - startX;
            const dy = endY - startY;
            const len = Math.sqrt(dx * dx + dy * dy);
            const segCount = Math.max(5, Math.floor(len / 15));
            const result = (0, spawner_1.spawnRope)(points, constraints, objects, startX, startY, endX, endY, segCount, nextPointId, nextObjId);
            nextPointId = result.nextPointId;
            nextObjId = result.nextObjId;
            break;
        }
        case 'cloth': {
            const dx = Math.abs(endX - startX);
            const dy = Math.abs(endY - startY);
            const spacing = 12;
            const cols = Math.max(4, Math.floor(dx / spacing));
            const rows = Math.max(3, Math.floor(dy / spacing));
            const result = (0, spawner_1.spawnCloth)(points, constraints, objects, Math.min(startX, endX), Math.min(startY, endY), cols, rows, spacing, clothPinPattern, nextPointId, nextObjId);
            nextPointId = result.nextPointId;
            nextObjId = result.nextObjId;
            break;
        }
        case 'softbody': {
            const radius = Math.max(20, Math.sqrt((endX - startX) ** 2 + (endY - startY) ** 2) / 2);
            const result = (0, spawner_1.spawnSoftBody)(points, constraints, objects, startX, startY, radius, 8, 3, nextPointId, nextObjId);
            nextPointId = result.nextPointId;
            nextObjId = result.nextObjId;
            break;
        }
        case 'ball': {
            const result = (0, spawner_1.spawnBall)(points, constraints, objects, startX, startY, 15, nextPointId, nextObjId);
            nextPointId = result.nextPointId;
            nextObjId = result.nextObjId;
            break;
        }
    }
}
function render() {
    // Motion blur
    if (constants_1.RENDER.motionBlur) {
        renderer.clear(0.15);
    }
    else {
        renderer.clear(1);
    }
    renderer.drawGrid();
    renderer.drawBounds();
    renderer.drawStaticShapes(staticShapes);
    renderer.drawObjects(objects, points, constraints, showStress);
    renderer.drawGrabHighlight(points, inputHandler.state.grabPoint);
    // Draw tear highlight
    if (inputHandler.state.rightDown || (inputHandler.state.mouseDown && inputHandler.state.modifier)) {
        renderer.drawTearHighlight(inputHandler.state.mouseX, inputHandler.state.mouseY, true);
    }
    // Draw spawn preview
    if (currentTool !== 'select' && inputHandler.state.mouseDown && pendingSpawn) {
        const dragLine = inputHandler.getDragLine();
        if (dragLine) {
            const colorMap = {
                rope: constants_1.COLORS.rope,
                cloth: constants_1.COLORS.cloth,
                softbody: constants_1.COLORS.softBody,
                ball: constants_1.COLORS.ball,
            };
            renderer.drawSpawnLine(dragLine.startX, dragLine.startY, dragLine.endX, dragLine.endY, colorMap[currentTool] || '#fff');
        }
    }
    renderer.drawHUD(fps, physicsTime, points.length, constraints.filter(c => c.active).length, paused);
    renderer.drawInstructions();
}
// ===== Expose on window =====
window.sim = sim;
// ===== Start =====
start();
function start() {
    init();
}
};

// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CANVAS = exports.RENDER = exports.COLORS = exports.PHYSICS = void 0;
// Tunable physics constants
exports.PHYSICS = {
    gravity: 980, // pixels/s²
    damping: 0.99, // velocity damping per step
    iterations: 6, // constraint relaxation iterations
    timestep: 1 / 60, // fixed physics timestep (seconds)
    maxDt: 0.05, // max frame delta before clamping (anti-explode)
    minDt: 1 / 120, // minimum timestep
    tearRadius: 30, // radius for tear/cut detection
    grabRadius: 25, // radius for grabbing points
    collisionPadding: 2, // extra padding for collision response
    energyDamp: 0.95, // energy damping for resting stability
    maxVelocity: 5000, // velocity clamp to prevent explosion
};
exports.COLORS = {
    bg: '#1a1a2e',
    grid: '#2a2a4a',
    wall: '#4a4a6a',
    rope: '#e74c3c',
    cloth: '#3498db',
    softBody: '#2ecc71',
    ball: '#f39c12',
    pin: '#e67e22',
    grab: '#9b59b6',
    tear: '#ff0044',
    stressGreen: '#00ff00',
    stressRed: '#ff0000',
    text: '#cccccc',
    toolbar: '#2c2c54',
    toolbarHover: '#3d3d6b',
    toolbarActive: '#4e4e8a',
};
exports.RENDER = {
    pointRadius: 2,
    clothPointSize: 0, // 0 = hidden for cloth
    showPoints: false,
    showStress: false,
    trailAlpha: 0.1,
    motionBlur: true,
};
exports.CANVAS = {
    toolbarHeight: 44,
    hudHeight: 20,
};
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// Core physics 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.collideStatic = collideStatic;
exports.collideBounds = collideBounds;
exports.checkNaN = checkNaN;
exports.kineticEnergy = kineticEnergy;
exports.findNearestPoint = findNearestPoint;
exports.findNearbyConstraints = findNearbyConstraints;
exports.constraintStress = constraintStress;
const constants_1 = require("./constants");
// Guard against NaN/Infinity
function safe(v) {
    if (!isFinite(v))
        return 0;
    return v;
}
function clamp(v, lo, hi) {
    return v < lo ? lo : v > hi ? hi : v;
}
// Verlet integration step
function integrate(points, dt, gravity, damping) {
    const dtSq = dt * dt;
    const maxVel = constants_1.PHYSICS.maxVelocity;
    for (let i = 0; i < points.length; i++) {
        const p = points[i];
        if (p.pinned)
            continue;
        // Verlet: x' = x + (x - xPrev) * damping + a * dt²
        const vx = (p.x - p.prevX) * damping;
        const vy = (p.y - p.prevY) * damping;
        // Clamp velocity to prevent explosion
        const speed = Math.sqrt(vx * vx + vy * vy);
        let cx = vx, cy = vy;
        if (speed > maxVel) {
            const scale = maxVel / speed;
            cx *= scale;
            cy *= scale;
        }
        p.prevX = p.x;
        p.prevY = p.y;
        p.x += cx;
        p.y += cy + gravity * dtSq;
    }
}
// Distance constraint solver
function solveConstraints(points, constraints, iterations) {
    for (let iter = 0; iter < iterations; iter++) {
        for (let i = 0; i < constraints.length; i++) {
            const c = constraints[i];
            if (!c.active)
                continue;
            const p1 = points[c.p1];
            const p2 = points[c.p2];
            if (!p1 || !p2)
                continue;
            const dx = p2.x - p1.x;
            const dy = p2.y - p1.y;
            const dist = Math.sqrt(dx * dx + dy * dy);
            // Guard against zero-length
            if (dist < 0.0001)
                continue;
            const diff = (c.restLength - dist) / dist;
            // Apply correction weighted by stiffness and pin status
            const correction = diff * c.stiffness * 0.5;
            const ox = dx * correction;
            const oy = dy * correction;
            if (!p1.pinned) {
                p1.x -= safe(ox);
                p1.y -= safe(oy);
            }
            if (!p2.pinned) {
                p2.x += safe(ox);
                p2.y += safe(oy);
            }
        }
    }
}
// Collision with static shapes
function collideStatic(points, shapes) {
    const pad = constants_1.PHYSICS.collisionPadding;
    const friction = 0.9;
    for (let i = 0; i < points.length; i++) {
        const p = points[i];
        if (p.pinned)
            continue;
        for (let j = 0; j < shapes.length; j++) {
            const shape = shapes[j];
            if ('radius' in shape && 'w' in shape) {
                // Static circle
                const c = shape;
                const dx = p.x - c.x;
                const dy = p.y - c.y;
                const dist = Math.sqrt(dx * dx + dy * dy);
                const minDist = c.radius + p.radius + pad;
                if (dist < minDist && dist > 0.001) {
                    const nx = dx / dist;
                    const ny = dy / dist;
                    const overlap = minDist - dist;
                    p.x += nx * overlap;
                    p.y += ny * overlap;
                    // Friction: reduce 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 += nx * normalVel * (1 - 0.5); // bounce
                        p.prevY += ny * normalVel * (1 - 0.5);
                        // Friction on tangential component
                        const tx = -ny;
                        const ty = nx;
                        const tanVel = vx * tx + vy * ty;
                        p.prevX += tx * tanVel * (1 - friction);
                        p.prevY += ty * tanVel * (1 - friction);
                    }
                }
            }
            else {
                // Static box (AABB)
                const b = shape;
                const cx = b.x + b.w / 2;
                const cy = b.y + b.h / 2;
                const hw = b.w / 2 + p.radius + pad;
                const hh = b.h / 2 + p.radius + pad;
                const dx = clamp(p.x, b.x, b.x + b.w);
                const dy = clamp(p.y, b.y, b.y + b.h);
                const distX = p.x - dx;
                const distY = p.y - dy;
                const dist = Math.sqrt(distX * distX + distY * distY);
                if (dist < p.radius + pad && dist > 0.001) {
                    const overlap = (p.radius + pad) - dist;
                    const nx = distX / dist;
                    const ny = distY / dist;
                    p.x += nx * overlap;
                    p.y += ny * overlap;
                    const vx = p.x - p.prevX;
                    const vy = p.y - p.prevY;
                    const normalVel = vx * nx + vy * ny;
                    if (normalVel < 0) {
                        p.prevX += nx * normalVel * (1 - 0.5);
                        p.prevY += ny * normalVel * (1 - 0.5);
                    }
                }
            }
        }
    }
}
// Boundary collision (walls and floor)
function collideBounds(points, width, height) {
    const pad = constants_1.PHYSICS.collisionPadding;
    const friction = 0.9;
    const bounce = 0.5;
    for (let i = 0; i < points.length; i++) {
        const p = points[i];
        if (p.pinned)
            continue;
        // Floor
        if (p.y > height - pad - p.radius) {
            p.y = height - pad - p.radius;
            const vx = p.x - p.prevX;
            const vy = p.y - p.prevY;
            // Dampen vertical velocity (bounce)
            p.prevY = p.y + vy * bounce;
            // Friction on horizontal
            p.prevX = p.x - vx * friction;
        }
        // Ceiling
        if (p.y < pad + p.radius) {
            p.y = pad + p.radius;
            p.prevY = p.y + (p.y - p.prevY) * bounce;
        }
        // Left wall
        if (p.x < pad + p.radius) {
            p.x = pad + p.radius;
            p.prevX = p.x + (p.x - p.prevX) * bounce;
            const vy = p.y - p.prevY;
            p.prevY = p.y - vy * friction;
        }
        // Right wall
        if (p.x > width - pad - p.radius) {
            p.x = width - pad - p.radius;
            p.prevX = p.x + (p.x - p.prevX) * bounce;
            const vy = p.y - p.prevY;
            p.prevY = p.y - vy * friction;
        }
    }
}
// Check for NaN in all points
function checkNaN(points) {
    for (let i = 0; i < points.length; i++) {
        const p = points[i];
        if (!isFinite(p.x) || !isFinite(p.y) || !isFinite(p.prevX) || !isFinite(p.prevY)) {
            return true;
        }
    }
    return false;
}
// Compute total kinetic energy
function kineticEnergy(points, dt) {
    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) / dt;
        const vy = (p.y - p.prevY) / dt;
        energy += 0.5 * p.mass * (vx * vx + vy * vy);
    }
    return energy;
}
// Find nearest point to position within radius
function findNearestPoint(points, x, y, radius) {
    let minDist = radius;
    let closest = -1;
    for (let i = 0; i < points.length; i++) {
        const p = points[i];
        const dx = p.x - x;
        const dy = p.y - y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < minDist) {
            minDist = dist;
            closest = i;
        }
    }
    return closest >= 0 ? closest : null;
}
// Find constraints near a position (for tearing)
function findNearbyConstraints(constraints, points, x, y, radius) {
    const torn = [];
    for (let i = 0; i < constraints.length; i++) {
        const c = constraints[i];
        if (!c.active)
            continue;
        const p1 = points[c.p1];
        const p2 = points[c.p2];
        if (!p1 || !p2)
            continue;
        // Distance from point to line segment
        const dx = p2.x - p1.x;
        const dy = p2.y - p1.y;
        const lenSq = dx * dx + dy * dy;
        if (lenSq < 0.0001)
            continue;
        // Project point onto line segment
        let t = ((x - p1.x) * dx + (y - p1.y) * dy) / lenSq;
        t = clamp(t, 0, 1);
        const projX = p1.x + t * dx;
        const projY = p1.y + t * dy;
        const dist = Math.sqrt((x - projX) ** 2 + (y - projY) ** 2);
        if (dist < radius) {
            torn.push(i);
        }
    }
    return torn;
}
// Compute stress ratio for a constraint
function constraintStress(constraint, points) {
    const p1 = points[constraint.p1];
    const p2 = points[constraint.p2];
    if (!p1 || !p2)
        return 0;
    const dx = p2.x - p1.x;
    const dy = p2.y - p1.y;
    const dist = Math.sqrt(dx * dx + dy * dy);
    if (constraint.restLength < 0.0001)
        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.computeConvexHull = computeConvexHull;
const constants_1 = require("./constants");
// Spawner: creates ropes, cloth, soft bodies, balls
// All coordinates are absolute canvas coordinates
function spawnRope(points, constraints, objects, startX, startY, endX, endY, segCount, nextPointId, nextObjId) {
    const dx = endX - startX;
    const dy = endY - startY;
    const len = Math.sqrt(dx * dx + dy * dy);
    const segLen = len / segCount;
    const pointStart = points.length;
    const objId = nextObjId++;
    for (let i = 0; i <= segCount; i++) {
        const t = i / segCount;
        const px = startX + dx * t;
        const py = startY + dy * t;
        points.push({
            x: px, y: py,
            prevX: px, prevY: py,
            pinned: i === 0,
            mass: 1,
            radius: 3,
            id: nextPointId++,
            objId,
        });
    }
    const constraintStart = constraints.length;
    for (let i = 0; i < segCount; i++) {
        constraints.push({
            p1: pointStart + i,
            p2: pointStart + i + 1,
            restLength: segLen,
            stiffness: 1,
            active: true,
            objId,
        });
    }
    objects.push({
        id: objId,
        type: 'rope',
        color: constants_1.COLORS.rope,
        pointStart,
        pointCount: segCount + 1,
        constraintStart,
        constraintCount: segCount,
    });
    return { nextPointId, nextObjId };
}
function spawnCloth(points, constraints, objects, x, y, cols, rows, spacing, pinPattern, nextPointId, nextObjId) {
    const objId = nextObjId++;
    const pointStart = points.length;
    // Create grid of points
    const grid = [];
    for (let r = 0; r < rows; r++) {
        grid[r] = [];
        for (let c = 0; c < cols; c++) {
            const px = x + c * spacing;
            const py = y + r * spacing;
            const pinned = pinPattern === 'fullEdge'
                ? r === 0
                : (r === 0 && (c === 0 || c === cols - 1));
            grid[r][c] = points.length;
            points.push({
                x: px, y: py,
                prevX: px, prevY: py,
                pinned,
                mass: 1,
                radius: 2,
                id: nextPointId++,
                objId,
            });
        }
    }
    const constraintStart = constraints.length;
    // Structural constraints (horizontal + vertical)
    for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
            // Right neighbor
            if (c < cols - 1) {
                constraints.push({
                    p1: grid[r][c],
                    p2: grid[r][c + 1],
                    restLength: spacing,
                    stiffness: 1,
                    active: true,
                    objId,
                });
            }
            // Bottom neighbor
            if (r < rows - 1) {
                constraints.push({
                    p1: grid[r][c],
                    p2: grid[r + 1][c],
                    restLength: spacing,
                    stiffness: 1,
                    active: true,
                    objId,
                });
            }
        }
    }
    // Shear constraints (diagonal)
    for (let r = 0; r < rows - 1; r++) {
        for (let c = 0; c < cols - 1; c++) {
            const diagLen = spacing * Math.sqrt(2);
            constraints.push({
                p1: grid[r][c],
                p2: grid[r + 1][c + 1],
                restLength: diagLen,
                stiffness: 0.5,
                active: true,
                objId,
            });
            constraints.push({
                p1: grid[r][c + 1],
                p2: grid[r + 1][c],
                restLength: diagLen,
                stiffness: 0.5,
                active: true,
                objId,
            });
        }
    }
    objects.push({
        id: objId,
        type: 'cloth',
        color: constants_1.COLORS.cloth,
        pointStart,
        pointCount: rows * cols,
        constraintStart,
        constraintCount: constraints.length - constraintStart,
    });
    return { nextPointId, nextObjId };
}
function spawnSoftBody(points, constraints, objects, cx, cy, radius, vertexCount, layers, nextPointId, nextObjId) {
    const objId = nextObjId++;
    const pointStart = points.length;
    // Create concentric rings of points
    const rings = [];
    for (let layer = 0; layer < layers; layer++) {
        const r = layer === 0 ? 0 : radius * (layer + 1) / layers;
        const verts = layer === 0 ? 1 : vertexCount;
        rings[layer] = [];
        for (let v = 0; v < verts; v++) {
            const angle = (v / verts) * Math.PI * 2 + (layer * Math.PI / vertexCount);
            const px = cx + Math.cos(angle) * r;
            const py = cy + Math.sin(angle) * r;
            rings[layer][v] = points.length;
            points.push({
                x: px, y: py,
                prevX: px, prevY: py,
                pinned: false,
                mass: 1,
                radius: 3,
                id: nextPointId++,
                objId,
            });
        }
    }
    const constraintStart = constraints.length;
    // Edge constraints (ring connections)
    for (let layer = 1; layer < layers; layer++) {
        for (let v = 0; v < vertexCount; v++) {
            const next = (v + 1) % vertexCount;
            const p1 = rings[layer][v];
            const p2 = rings[layer][next];
            const a1 = (v / vertexCount) * Math.PI * 2 + (layer * Math.PI / vertexCount);
            const a2 = (next / vertexCount) * Math.PI * 2 + (layer * Math.PI / vertexCount);
            const r = radius * layer / layers;
            const chord = 2 * r * Math.sin(Math.PI / vertexCount);
            constraints.push({
                p1, p2,
                restLength: chord,
                stiffness: 1,
                active: true,
                objId,
            });
        }
    }
    // Radial constraints (between layers)
    for (let layer = 1; layer < layers; layer++) {
        for (let v = 0; v < vertexCount; v++) {
            const inner = layer === 1 ? rings[0][0] : rings[layer - 1][v];
            const outer = rings[layer][v];
            const rInner = layer === 1 ? 0 : radius * (layer - 1) / layers;
            const rOuter = radius * layer / layers;
            const radialLen = rOuter - rInner;
            constraints.push({
                p1: inner,
                p2: outer,
                restLength: radialLen,
                stiffness: 1,
                active: true,
                objId,
            });
        }
    }
    // Internal brace constraints (cross-bracing for shape stability)
    for (let layer = 1; layer < layers - 1; layer++) {
        for (let v = 0; v < vertexCount; v++) {
            const next = (v + 1) % vertexCount;
            constraints.push({
                p1: rings[layer][v],
                p2: rings[layer + 1][next],
                restLength: radius / layers,
                stiffness: 0.5,
                active: true,
                objId,
            });
        }
    }
    objects.push({
        id: objId,
        type: 'softbody',
        color: constants_1.COLORS.softBody,
        pointStart,
        pointCount: points.length - pointStart,
        constraintStart,
        constraintCount: constraints.length - constraintStart,
    });
    return { nextPointId, nextObjId };
}
function spawnBall(points, constraints, objects, x, y, radius, nextPointId, nextObjId) {
    const objId = nextObjId++;
    points.push({
        x, y,
        prevX: x, prevY: y,
        pinned: false,
        mass: 2,
        radius,
        id: nextPointId++,
        objId,
    });
    objects.push({
        id: objId,
        type: 'ball',
        color: constants_1.COLORS.ball,
        pointStart: points.length - 1,
        pointCount: 1,
        constraintStart: constraints.length,
        constraintCount: 0,
    });
    return { nextPointId, nextObjId };
}
// Compute convex hull for soft body rendering
function computeConvexHull(indices, points) {
    if (indices.length < 3)
        return indices;
    // Simple convex hull (Graham scan)
    const pts = indices.map(i => ({ x: points[i].x, y: points[i].y, idx: i }));
    // Find bottom-most point
    let bottom = 0;
    for (let i = 1; i < pts.length; i++) {
        if (pts[i].y > pts[bottom].y || (pts[i].y === pts[bottom].y && pts[i].x > pts[bottom].x)) {
            bottom = i;
        }
    }
    [pts[0], pts[bottom]] = [pts[bottom], pts[0]];
    // Sort by polar angle
    const pivot = pts[0];
    pts.slice(1).sort((a, b) => {
        const angleA = Math.atan2(a.y - pivot.y, a.x - pivot.x);
        const angleB = Math.atan2(b.y - pivot.y, b.x - pivot.x);
        return angleA - angleB;
    });
    const hull = [pts[0].idx, pts[1].idx];
    for (let i = 2; i < pts.length; i++) {
        while (hull.length > 1) {
            const top = hull[hull.length - 1];
            const second = hull[hull.length - 2];
            const cross = (pts[i].x - points[second].x) * (points[top].y - points[second].y)
                - (pts[i].y - points[second].y) * (points[top].x - points[second].x);
            if (cross > 0)
                break;
            hull.pop();
        }
        hull.push(pts[i].idx);
    }
    return hull;
}
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputHandler = void 0;
const constants_1 = require("./constants");
const physics_1 = require("./physics");
class InputHandler {
    constructor(canvas) {
        this.state = {
            mouseX: 0,
            mouseY: 0,
            mouseDown: false,
            rightDown: false,
            grabPoint: null,
            tool: 'select',
            paused: false,
            stepOnce: false,
            modifier: false,
        };
        this.prevGrabX = 0;
        this.prevGrabY = 0;
        this.grabVelX = 0;
        this.grabVelY = 0;
        this.dragStartX = 0;
        this.dragStartY = 0;
        this.isDragging = false;
        this.canvas = canvas;
        this.bindEvents();
    }
    getCanvasPos(e) {
        const rect = this.canvas.getBoundingClientRect();
        return {
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
        };
    }
    bindEvents() {
        this.canvas.addEventListener('mousedown', (e) => this.onMouseDown(e));
        this.canvas.addEventListener('mousemove', (e) => this.onMouseMove(e));
        this.canvas.addEventListener('mouseup', (e) => this.onMouseUp(e));
        this.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
        // Keyboard
        window.addEventListener('keydown', (e) => this.onKeyDown(e));
        window.addEventListener('keyup', (e) => this.onKeyUp(e));
    }
    onMouseDown(e) {
        const pos = this.getCanvasPos(e);
        if (e.button === 0) {
            this.state.mouseDown = true;
            this.prevGrabX = pos.x;
            this.prevGrabY = pos.y;
            this.grabVelX = 0;
            this.grabVelY = 0;
            this.dragStartX = pos.x;
            this.dragStartY = pos.y;
            this.isDragging = false;
        }
        else if (e.button === 2) {
            this.state.rightDown = true;
        }
    }
    onMouseMove(e) {
        const pos = this.getCanvasPos(e);
        this.state.mouseX = pos.x;
        this.state.mouseY = pos.y;
        // Track grab velocity
        if (this.state.grabPoint !== null) {
            this.grabVelX = (pos.x - this.prevGrabX) / constants_1.PHYSICS.timestep;
            this.grabVelY = (pos.y - this.prevGrabY) / constants_1.PHYSICS.timestep;
            this.prevGrabX = pos.x;
            this.prevGrabY = pos.y;
        }
        // Track if we're dragging (moved more than a few pixels)
        if (this.state.mouseDown && !this.isDragging) {
            const dx = pos.x - this.dragStartX;
            const dy = pos.y - this.dragStartY;
            if (Math.sqrt(dx * dx + dy * dy) > 5) {
                this.isDragging = true;
            }
        }
    }
    onMouseUp(e) {
        if (e.button === 0) {
            this.state.mouseDown = false;
        }
        else if (e.button === 2) {
            this.state.rightDown = false;
        }
    }
    onKeyDown(e) {
        if (e.code === 'Space') {
            e.preventDefault();
            this.state.paused = !this.state.paused;
        }
        if (e.code === 'ArrowRight' && this.state.paused) {
            e.preventDefault();
            this.state.stepOnce = true;
        }
        if (e.key === 'Shift' || e.key === 'Control') {
            this.state.modifier = true;
        }
        if (e.key === 't' || e.key === 'T') {
            this.state.tool = this.state.tool === 'tear' ? 'select' : 'tear';
        }
    }
    onKeyUp(e) {
        if (e.key === 'Shift' || e.key === 'Control') {
            this.state.modifier = false;
        }
    }
    // Process input each frame - returns actions to take
    process(points, constraints, width, height) {
        const result = {
            grabPoint: this.state.grabPoint,
            releaseGrab: false,
            tearConstraints: [],
            spawnRequest: null,
            pinToggle: null,
        };
        // Handle tear mode (right-drag or modifier+left-drag)
        if (this.state.rightDown || (this.state.mouseDown && this.state.modifier)) {
            const torn = (0, physics_1.findNearbyConstraints)(constraints, points, this.state.mouseX, this.state.mouseY, constants_1.PHYSICS.tearRadius);
            result.tearConstraints = torn;
            return result;
        }
        // Handle grab
        if (this.state.mouseDown) {
            if (this.state.grabPoint === null && !this.isDragging) {
                // Try to grab nearest point
                const idx = (0, physics_1.findNearestPoint)(points, this.state.mouseX, this.state.mouseY, constants_1.PHYSICS.grabRadius);
                if (idx !== null) {
                    this.state.grabPoint = idx;
                    this.prevGrabX = this.state.mouseX;
                    this.prevGrabY = this.state.mouseY;
                    result.grabPoint = idx;
                }
            }
            // Move grabbed point
            if (this.state.grabPoint !== null) {
                const p = points[this.state.grabPoint];
                p.x = this.state.mouseX;
                p.y = this.state.mouseY;
                // Update prev to match (so velocity is from drag speed)
                p.prevX = this.state.mouseX - this.grabVelX * constants_1.PHYSICS.timestep;
                p.prevY = this.state.mouseY - this.grabVelY * constants_1.PHYSICS.timestep;
            }
        }
        else if (this.state.grabPoint !== null) {
            // Released - throw with drag velocity
            result.releaseGrab = true;
            result.grabPoint = this.state.grabPoint;
            this.state.grabPoint = null;
        }
        return result;
    }
    // Apply throw velocity when releasing a grab
    applyThrow(points, grabPoint) {
        const p = points[grabPoint];
        if (!p)
            return;
        // Set prev position to create throw velocity
        p.prevX = p.x - this.grabVelX * constants_1.PHYSICS.timestep;
        p.prevY = p.y - this.grabVelY * constants_1.PHYSICS.timestep;
    }
    // Get spawn request when user clicks in spawn mode
    getSpawnRequest() {
        if (!this.state.mouseDown || this.isDragging)
            return null;
        // Only fire on click (no drag)
        return { x: this.state.mouseX, y: this.state.mouseY };
    }
    getDragLine() {
        if (!this.isDragging)
            return null;
        return {
            startX: this.dragStartX,
            startY: this.dragStartY,
            endX: this.state.mouseX,
            endY: this.state.mouseY,
        };
    }
    getGrabVelX() { return this.grabVelX; }
    getGrabVelY() { return this.grabVelY; }
}
exports.InputHandler = InputHandler;
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
const constants_1 = require("./constants");
const physics_1 = require("./physics");
const spawner_1 = require("./spawner");
class Renderer {
    constructor(canvas) {
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');
        this.width = canvas.width;
        this.height = canvas.height;
    }
    resize(width, height) {
        this.width = width;
        this.height = height;
        this.canvas.width = width;
        this.canvas.height = height;
    }
    clear(alpha) {
        this.ctx.fillStyle = constants_1.COLORS.bg;
        this.ctx.fillRect(0, 0, this.width, this.height);
    }
    drawGrid() {
        this.ctx.strokeStyle = constants_1.COLORS.grid;
        this.ctx.lineWidth = 0.5;
        this.ctx.globalAlpha = 0.3;
        const gridSize = 40;
        for (let x = 0; x < this.width; x += gridSize) {
            this.ctx.beginPath();
            this.ctx.moveTo(x, 0);
            this.ctx.lineTo(x, this.height);
            this.ctx.stroke();
        }
        for (let y = 0; y < this.height; y += gridSize) {
            this.ctx.beginPath();
            this.ctx.moveTo(0, y);
            this.ctx.lineTo(this.width, y);
            this.ctx.stroke();
        }
        this.ctx.globalAlpha = 1;
    }
    drawBounds() {
        this.ctx.strokeStyle = constants_1.COLORS.wall;
        this.ctx.lineWidth = 3;
        this.ctx.strokeRect(1, 1, this.width - 2, this.height - 2);
        // Floor highlight
        this.ctx.fillStyle = 'rgba(255,255,255,0.03)';
        this.ctx.fillRect(0, this.height - 20, this.width, 20);
    }
    drawStaticShapes(shapes) {
        for (const shape of shapes) {
            if ('radius' in shape && 'w' in shape) {
                const c = shape;
                // Shadow
                this.ctx.beginPath();
                this.ctx.arc(c.x + 3, c.y + 3, c.radius, 0, Math.PI * 2);
                this.ctx.fillStyle = 'rgba(0,0,0,0.3)';
                this.ctx.fill();
                // Shape
                this.ctx.beginPath();
                this.ctx.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
                this.ctx.fillStyle = 'rgba(100, 100, 140, 0.4)';
                this.ctx.fill();
                this.ctx.strokeStyle = constants_1.COLORS.wall;
                this.ctx.lineWidth = 2;
                this.ctx.stroke();
                // Highlight
                this.ctx.beginPath();
                this.ctx.arc(c.x - c.radius * 0.2, c.y - c.radius * 0.2, c.radius * 0.3, 0, Math.PI * 2);
                this.ctx.fillStyle = 'rgba(255,255,255,0.1)';
                this.ctx.fill();
            }
            else {
                const b = shape;
                // Shadow
                this.ctx.fillStyle = 'rgba(0,0,0,0.3)';
                this.ctx.fillRect(b.x + 3, b.y + 3, b.w, b.h);
                // Shape
                this.ctx.fillStyle = 'rgba(100, 100, 140, 0.4)';
                this.ctx.fillRect(b.x, b.y, b.w, b.h);
                this.ctx.strokeStyle = constants_1.COLORS.wall;
                this.ctx.lineWidth = 2;
                this.ctx.strokeRect(b.x, b.y, b.w, b.h);
            }
        }
    }
    drawConstraint(c, points, color, showStress) {
        const p1 = points[c.p1];
        const p2 = points[c.p2];
        if (!p1 || !p2 || !c.active)
            return;
        this.ctx.beginPath();
        this.ctx.moveTo(p1.x, p1.y);
        this.ctx.lineTo(p2.x, p2.y);
        if (showStress) {
            const stress = (0, physics_1.constraintStress)(c, points);
            const t = Math.min(Math.max((stress - 0.9) / 0.3, 0), 1);
            const r = Math.floor(t * 255);
            const g = Math.floor(255 * (1 - t));
            this.ctx.strokeStyle = `rgb(${r},${g},50)`;
        }
        else {
            this.ctx.strokeStyle = color;
        }
        this.ctx.lineWidth = c.stiffness > 0.8 ? 1.5 : 1;
        this.ctx.globalAlpha = 0.6;
        this.ctx.stroke();
        this.ctx.globalAlpha = 1;
    }
    drawPoint(p, color, radius) {
        this.ctx.beginPath();
        this.ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
        this.ctx.fillStyle = color;
        this.ctx.fill();
    }
    drawPin(p) {
        const x = p.x, y = p.y;
        // Cross
        this.ctx.strokeStyle = constants_1.COLORS.pin;
        this.ctx.lineWidth = 2;
        this.ctx.beginPath();
        this.ctx.moveTo(x - 6, y - 6);
        this.ctx.lineTo(x + 6, y + 6);
        this.ctx.moveTo(x + 6, y - 6);
        this.ctx.lineTo(x - 6, y + 6);
        this.ctx.stroke();
        // Circle
        this.ctx.beginPath();
        this.ctx.arc(x, y, 4, 0, Math.PI * 2);
        this.ctx.fillStyle = constants_1.COLORS.pin;
        this.ctx.fill();
        this.ctx.strokeStyle = '#fff';
        this.ctx.lineWidth = 1;
        this.ctx.stroke();
    }
    // Draw cloth as filled mesh with nice shading
    drawCloth(obj, points, constraints, showStress) {
        // We need to reconstruct the grid from the points
        // Cloth points are stored in row-major order
        // Find grid dimensions from constraint count
        const pCount = obj.pointCount;
        // Estimate cols and rows from structural constraints
        // A grid of CxR has (C-1)*R + (R-1)*C structural constraints
        // For simplicity, try common sizes
        let cols = 0, rows = 0;
        for (let c = 3; c <= 30; c++) {
            for (let r = 3; r <= 30; r++) {
                if (c * r === pCount) {
                    cols = c;
                    rows = r;
                    break;
                }
            }
            if (cols > 0)
                break;
        }
        if (cols > 0 && rows > 0) {
            // Draw filled triangles for each cell
            for (let r = 0; r < rows - 1; r++) {
                for (let c = 0; c < cols - 1; c++) {
                    const i00 = obj.pointStart + r * cols + c;
                    const i10 = obj.pointStart + r * cols + c + 1;
                    const i01 = obj.pointStart + (r + 1) * cols + c;
                    const i11 = obj.pointStart + (r + 1) * cols + c + 1;
                    const p00 = points[i00];
                    const p10 = points[i10];
                    const p01 = points[i01];
                    const p11 = points[i11];
                    if (!p00 || !p10 || !p01 || !p11)
                        continue;
                    // Compute cell "height" for shading (higher = lighter)
                    const avgY = (p00.y + p10.y + p01.y + p11.y) / 4;
                    const shade = Math.max(0.15, Math.min(0.45, 0.45 - (avgY / this.height) * 0.2));
                    // Draw two triangles per cell
                    this.ctx.fillStyle = hexToRgba(obj.color, shade);
                    this.ctx.beginPath();
                    this.ctx.moveTo(p00.x, p00.y);
                    this.ctx.lineTo(p10.x, p10.y);
                    this.ctx.lineTo(p01.x, p01.y);
                    this.ctx.closePath();
                    this.ctx.fill();
                    this.ctx.beginPath();
                    this.ctx.moveTo(p10.x, p10.y);
                    this.ctx.lineTo(p11.x, p11.y);
                    this.ctx.lineTo(p01.x, p01.y);
                    this.ctx.closePath();
                    this.ctx.fill();
                }
            }
            // Draw grid lines on top
            for (let i = obj.constraintStart; i < obj.constraintStart + obj.constraintCount; i++) {
                this.drawConstraint(constraints[i], points, obj.color, showStress);
            }
        }
        else {
            // Fallback: just draw constraints
            for (let i = obj.constraintStart; i < obj.constraintStart + obj.constraintCount; i++) {
                this.drawConstraint(constraints[i], points, obj.color, showStress);
            }
        }
        // Draw pins
        for (let i = obj.pointStart; i < obj.pointStart + obj.pointCount; i++) {
            if (points[i].pinned)
                this.drawPin(points[i]);
        }
    }
    drawRope(obj, points, constraints, showStress) {
        // Draw rope as a thick line
        this.ctx.beginPath();
        this.ctx.moveTo(points[obj.pointStart].x, points[obj.pointStart].y);
        for (let i = 1; i < obj.pointCount; i++) {
            this.ctx.lineTo(points[obj.pointStart + i].x, points[obj.pointStart + i].y);
        }
        this.ctx.strokeStyle = obj.color;
        this.ctx.lineWidth = 4;
        this.ctx.lineCap = 'round';
        this.ctx.lineJoin = 'round';
        this.ctx.globalAlpha = 0.8;
        this.ctx.stroke();
        this.ctx.globalAlpha = 1;
        // Draw constraint lines if stress mode
        if (showStress) {
            for (let i = obj.constraintStart; i < obj.constraintStart + obj.constraintCount; i++) {
                this.drawConstraint(constraints[i], points, obj.color, true);
            }
        }
        // Draw points
        if (constants_1.RENDER.showPoints) {
            for (let i = obj.pointStart; i < obj.pointStart + obj.pointCount; i++) {
                this.drawPoint(points[i], obj.color, constants_1.RENDER.pointRadius);
                if (points[i].pinned)
                    this.drawPin(points[i]);
            }
        }
        else {
            // Just draw pins
            for (let i = obj.pointStart; i < obj.pointStart + obj.pointCount; i++) {
                if (points[i].pinned)
                    this.drawPin(points[i]);
            }
        }
    }
    drawSoftBody(obj, points, constraints, showStress) {
        const objPoints = [];
        for (let i = obj.pointStart; i < obj.pointStart + obj.pointCount; i++) {
            objPoints.push(i);
        }
        // Draw filled body with convex hull
        const hull = (0, spawner_1.computeConvexHull)(objPoints, points);
        if (hull.length > 2) {
            // Shadow
            this.ctx.beginPath();
            this.ctx.moveTo(points[hull[0]].x + 3, points[hull[0]].y + 3);
            for (let i = 1; i < hull.length; i++) {
                this.ctx.lineTo(points[hull[i]].x + 3, points[hull[i]].y + 3);
            }
            this.ctx.closePath();
            this.ctx.fillStyle = 'rgba(0,0,0,0.2)';
            this.ctx.fill();
            // Main body
            this.ctx.beginPath();
            this.ctx.moveTo(points[hull[0]].x, points[hull[0]].y);
            for (let i = 1; i < hull.length; i++) {
                this.ctx.lineTo(points[hull[i]].x, points[hull[i]].y);
            }
            this.ctx.closePath();
            // Gradient fill
            const cx = points.reduce((sum, p) => sum + p.x, 0) / points.length;
            const cy = points.reduce((sum, p) => sum + p.y, 0) / points.length;
            const grad = this.ctx.createRadialGradient(cx - 10, cy - 10, 0, cx, cy, 50);
            grad.addColorStop(0, hexToRgba(obj.color, 0.35));
            grad.addColorStop(1, hexToRgba(obj.color, 0.15));
            this.ctx.fillStyle = grad;
            this.ctx.fill();
            this.ctx.strokeStyle = obj.color;
            this.ctx.lineWidth = 2;
            this.ctx.stroke();
        }
        // Draw internal constraints
        for (let i = obj.constraintStart; i < obj.constraintStart + obj.constraintCount; i++) {
            this.drawConstraint(constraints[i], points, obj.color, showStress);
        }
        // Draw points
        for (let i = obj.pointStart; i < obj.pointStart + obj.pointCount; i++) {
            this.drawPoint(points[i], obj.color, constants_1.RENDER.pointRadius);
            if (points[i].pinned)
                this.drawPin(points[i]);
        }
    }
    drawBall(obj, points) {
        const p = points[obj.pointStart];
        if (!p)
            return;
        // Shadow
        this.ctx.beginPath();
        this.ctx.arc(p.x + 3, p.y + 3, p.radius, 0, Math.PI * 2);
        this.ctx.fillStyle = 'rgba(0,0,0,0.3)';
        this.ctx.fill();
        // Ball
        this.ctx.beginPath();
        this.ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
        const grad = this.ctx.createRadialGradient(p.x - p.radius * 0.3, p.y - p.radius * 0.3, 0, p.x, p.y, p.radius);
        grad.addColorStop(0, hexToRgba(obj.color, 0.6));
        grad.addColorStop(1, hexToRgba(obj.color, 0.2));
        this.ctx.fillStyle = grad;
        this.ctx.fill();
        this.ctx.strokeStyle = obj.color;
        this.ctx.lineWidth = 2;
        this.ctx.stroke();
        // Highlight
        this.ctx.beginPath();
        this.ctx.arc(p.x - p.radius * 0.25, p.y - p.radius * 0.25, p.radius * 0.2, 0, Math.PI * 2);
        this.ctx.fillStyle = 'rgba(255,255,255,0.3)';
        this.ctx.fill();
        if (p.pinned)
            this.drawPin(p);
    }
    drawObjects(objects, points, constraints, showStress) {
        for (const obj of objects) {
            switch (obj.type) {
                case 'rope':
                    this.drawRope(obj, points, constraints, showStress);
                    break;
                case 'cloth':
                    this.drawCloth(obj, points, constraints, showStress);
                    break;
                case 'softbody':
                    this.drawSoftBody(obj, points, constraints, showStress);
                    break;
                case 'ball':
                    this.drawBall(obj, points);
                    break;
            }
        }
    }
    drawGrabHighlight(points, grabPoint) {
        if (grabPoint === null)
            return;
        const p = points[grabPoint];
        if (!p)
            return;
        // Pulsing ring
        const time = Date.now() / 500;
        const radius = constants_1.PHYSICS.grabRadius + Math.sin(time) * 3;
        this.ctx.beginPath();
        this.ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
        this.ctx.strokeStyle = constants_1.COLORS.grab;
        this.ctx.lineWidth = 2;
        this.ctx.setLineDash([4, 4]);
        this.ctx.stroke();
        this.ctx.setLineDash([]);
        // Glow
        this.ctx.beginPath();
        this.ctx.arc(p.x, p.y, 8, 0, Math.PI * 2);
        this.ctx.fillStyle = 'rgba(155, 89, 182, 0.4)';
        this.ctx.fill();
    }
    drawTearHighlight(mouseX, mouseY, tearing) {
        if (!tearing)
            return;
        this.ctx.beginPath();
        this.ctx.arc(mouseX, mouseY, constants_1.PHYSICS.tearRadius, 0, Math.PI * 2);
        this.ctx.strokeStyle = constants_1.COLORS.tear;
        this.ctx.lineWidth = 2;
        this.ctx.setLineDash([4, 4]);
        this.ctx.stroke();
        this.ctx.setLineDash([]);
        // Red glow
        this.ctx.beginPath();
        this.ctx.arc(mouseX, mouseY, constants_1.PHYSICS.tearRadius, 0, Math.PI * 2);
        this.ctx.fillStyle = 'rgba(255, 0, 68, 0.1)';
        this.ctx.fill();
    }
    drawHUD(fps, physicsTime, pointCount, constraintCount, paused) {
        this.ctx.fillStyle = 'rgba(0,0,0,0.5)';
        this.ctx.fillRect(0, this.height - 24, this.width, 24);
        this.ctx.fillStyle = constants_1.COLORS.text;
        this.ctx.font = '12px monospace';
        this.ctx.fillText(`FPS: ${fps}`, 10, this.height - 8);
        this.ctx.fillText(`Physics: ${physicsTime.toFixed(1)}ms`, 80, this.height - 8);
        this.ctx.fillText(`Points: ${pointCount}`, 180, this.height - 8);
        this.ctx.fillText(`Constraints: ${constraintCount}`, 290, this.height - 8);
        if (paused) {
            this.ctx.fillStyle = '#ff6b6b';
            this.ctx.font = 'bold 14px monospace';
            this.ctx.fillText('⏸ PAUSED (Space to resume, → to step)', 420, this.height - 8);
        }
    }
    drawSpawnLine(startX, startY, endX, endY, color) {
        this.ctx.beginPath();
        this.ctx.moveTo(startX, startY);
        this.ctx.lineTo(endX, endY);
        this.ctx.strokeStyle = color;
        this.ctx.lineWidth = 2;
        this.ctx.setLineDash([6, 4]);
        this.ctx.stroke();
        this.ctx.setLineDash([]);
        // Start point
        this.ctx.beginPath();
        this.ctx.arc(startX, startY, 5, 0, Math.PI * 2);
        this.ctx.fillStyle = color;
        this.ctx.fill();
        // End point
        this.ctx.beginPath();
        this.ctx.arc(endX, endY, 5, 0, Math.PI * 2);
        this.ctx.fillStyle = color;
        this.ctx.globalAlpha = 0.5;
        this.ctx.fill();
        this.ctx.globalAlpha = 1;
    }
    drawInstructions() {
        this.ctx.fillStyle = 'rgba(255,255,255,0.15)';
        this.ctx.font = '11px monospace';
        this.ctx.textAlign = 'right';
        this.ctx.fillText('Space: Pause | Shift+Drag: Tear | Right-drag: Tear | T: Toggle tear mode', this.width - 10, this.height - 8);
        this.ctx.textAlign = 'left';
    }
}
exports.Renderer = Renderer;
// Helper: hex color to rgba string
function hexToRgba(hex, alpha) {
    const r = parseInt(hex.slice(1, 3), 16);
    const g = parseInt(hex.slice(3, 5), 16);
    const b = parseInt(hex.slice(5, 7), 16);
    return `rgba(${r},${g},${b},${alpha})`;
}
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Toolbar = void 0;
const constants_1 = require("./constants");
class Toolbar {
    constructor(container) {
        this.buttons = new Map();
        this.currentTool = 'select';
        this.clothPinPattern = 'corners';
        this.onToolChange = null;
        this.onReset = null;
        this.onGravityChange = null;
        this.onIterChange = null;
        this.onPinPatternChange = null;
        this.onStressToggle = null;
        this.onWindChange = null;
        this.onPointsToggle = null;
        this.btnStyle = `
    padding: 4px 10px;
    border: 1px solid ${constants_1.COLORS.wall};
    background: ${constants_1.COLORS.toolbar};
    color: ${constants_1.COLORS.text};
    border-radius: 4px;
    cursor: pointer;
    font-size: 12px;
    white-space: nowrap;
    transition: background 0.15s;
  `;
        this.container = container;
        this.build();
    }
    setOnToolChange(cb) { this.onToolChange = cb; }
    setOnReset(cb) { this.onReset = cb; }
    setOnGravityChange(cb) { this.onGravityChange = cb; }
    setOnIterChange(cb) { this.onIterChange = cb; }
    setOnPinPatternChange(cb) { this.onPinPatternChange = cb; }
    setOnStressToggle(cb) { this.onStressToggle = cb; }
    setOnWindChange(cb) { this.onWindChange = cb; }
    setOnPointsToggle(cb) { this.onPointsToggle = cb; }
    makeButton(label, id, title) {
        const btn = document.createElement('button');
        btn.textContent = label;
        btn.title = title || label;
        btn.style.cssText = this.btnStyle;
        btn.onmouseenter = () => btn.style.background = constants_1.COLORS.toolbarHover;
        btn.onmouseleave = () => {
            if (!btn.dataset.active)
                btn.style.background = constants_1.COLORS.toolbar;
        };
        btn.onclick = () => {
            // Flash effect
            btn.style.background = constants_1.COLORS.toolbarActive;
            setTimeout(() => {
                if (!btn.dataset.active)
                    btn.style.background = constants_1.COLORS.toolbar;
            }, 150);
        };
        this.container.appendChild(btn);
        this.buttons.set(id, btn);
        return btn;
    }
    makeSlider(label, min, max, step, value) {
        const wrap = document.createElement('div');
        wrap.style.cssText = 'display:flex;align-items:center;gap:4px;font-size:11px;color:' + constants_1.COLORS.text;
        const lbl = document.createElement('span');
        lbl.textContent = label;
        lbl.style.minWidth = '50px';
        wrap.appendChild(lbl);
        const slider = document.createElement('input');
        slider.type = 'range';
        slider.min = String(min);
        slider.max = String(max);
        slider.step = String(step);
        slider.value = String(value);
        slider.style.cssText = 'width:70px;height:14px;accent-color:#6c6cff';
        wrap.appendChild(slider);
        const val = document.createElement('span');
        val.textContent = String(value);
        val.style.cssText = 'min-width:35px;text-align:right;font-family:monospace';
        wrap.appendChild(val);
        slider.oninput = () => {
            val.textContent = String(parseFloat(slider.value));
        };
        this.container.appendChild(wrap);
        return slider;
    }
    makeSeparator() {
        const sep = document.createElement('div');
        sep.style.cssText = 'width:1px;height:22px;background:' + constants_1.COLORS.wall + ';margin:0 2px';
        this.container.appendChild(sep);
    }
    build() {
        // Tool buttons
        const tools = [
            { label: '🖱 Select', tool: 'select' },
            { label: '🪢 Rope', tool: 'rope' },
            { label: '🧵 Cloth', tool: 'cloth' },
            { label: '🟢 Blob', tool: 'softbody' },
            { label: '🔴 Ball', tool: 'ball' },
        ];
        for (const t of tools) {
            const btn = this.makeButton(t.label, 'tool-' + t.tool);
            btn.onclick = () => {
                this.currentTool = t.tool;
                this.updateToolButtons();
                if (this.onToolChange)
                    this.onToolChange(t.tool);
            };
            if (t.tool === 'select') {
                btn.style.background = constants_1.COLORS.toolbarActive;
                btn.dataset.active = '1';
            }
        }
        this.makeSeparator();
        // Cloth pin pattern
        const pinBtn = this.makeButton('Pin: Corners', 'pin-pattern');
        pinBtn.onclick = () => {
            this.clothPinPattern = this.clothPinPattern === 'corners' ? 'fullEdge' : 'corners';
            pinBtn.textContent = this.clothPinPattern === 'corners' ? 'Pin: Corners' : 'Pin: Full Edge';
            if (this.onPinPatternChange)
                this.onPinPatternChange(this.clothPinPattern);
        };
        this.makeSeparator();
        // Sliders
        this.gravitySlider = this.makeSlider('Gravity', 0, 2000, 10, constants_1.PHYSICS.gravity);
        this.gravitySlider.oninput = () => {
            if (this.onGravityChange)
                this.onGravityChange(parseFloat(this.gravitySlider.value));
        };
        this.iterSlider = this.makeSlider('Iterations', 1, 15, 1, constants_1.PHYSICS.iterations);
        this.iterSlider.oninput = () => {
            if (this.onIterChange)
                this.onIterChange(parseInt(this.iterSlider.value));
        };
        this.windSlider = this.makeSlider('Wind', -500, 500, 10, 0);
        this.windSlider.oninput = () => {
            if (this.onWindChange)
                this.onWindChange(parseFloat(this.windSlider.value));
        };
        this.makeSeparator();
        // Toggles
        const stressBtn = this.makeButton('🔴 Stress', 'stress');
        stressBtn.onclick = () => {
            if (this.onStressToggle)
                this.onStressToggle();
        };
        const pointsBtn = this.makeButton('⚫ Points', 'points');
        pointsBtn.onclick = () => {
            if (this.onPointsToggle)
                this.onPointsToggle();
        };
        this.makeSeparator();
        // Reset
        const resetBtn = this.makeButton('🔄 Reset', 'reset');
        resetBtn.onclick = () => {
            if (this.onReset)
                this.onReset();
        };
    }
    updateToolButtons() {
        for (const [id, btn] of this.buttons) {
            if (id.startsWith('tool-')) {
                const isActive = id === 'tool-' + this.currentTool;
                btn.style.background = isActive ? constants_1.COLORS.toolbarActive : constants_1.COLORS.toolbar;
                btn.dataset.active = isActive ? '1' : '';
            }
        }
    }
    getTool() { return this.currentTool; }
    getPinPattern() { return this.clothPinPattern; }
    getGravity() { return parseFloat(this.gravitySlider.value); }
    getIterations() { return parseInt(this.iterSlider.value); }
    getWind() { return parseFloat(this.windSlider.value); }
}
exports.Toolbar = Toolbar;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"7d7d839c-5522-4399-9a07-71ebee11844b","tokensIn":1516947,"tokensOut":39453,"tokensTotal":1556400,"cost":0,"turns":47,"toolCalls":49,"failedToolCalls":0,"timestamp":"2026-08-01T16:30:11.763Z"} -->