← Physics Playground results

Physics Playground

ThinkingCap Qwen3.6 27B GGUF · 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 38,217 bytes · SHA-256 c563b3e17203
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>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: #111; font-family: 'Segoe UI', system-ui, sans-serif; }
#app { position: relative; width: 100%; height: 100%; }
#canvas { display: block; width: 100%; height: 100%; cursor: crosshair; }

#toolbar {
  position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 4px;
  background: rgba(20,20,30,0.85); backdrop-filter: blur(6px);
  padding: 6px 12px; border-radius: 8px;
  border: 1px solid rgba(255,255,255,0.08);
  z-index: 10;
}
#toolbar button {
  background: rgba(255,255,255,0.08); color: #ccc; border: 1px solid transparent;
  padding: 5px 10px; border-radius: 5px; cursor: pointer; font-size: 14px;
  transition: background 0.15s, border-color 0.15s;
}
#toolbar button:hover { background: rgba(255,255,255,0.15); }
#toolbar button.active { border-color: #5af; color: #5af; background: rgba(85,170,255,0.12); }
#toolbar .sep { color: rgba(255,255,255,0.2); margin: 0 4px; user-select: none; }
#toolbar label {
  color: #999; font-size: 12px; display: flex; align-items: center; gap: 3px;
  margin: 0 2px;
}
#toolbar input[type="range"] { width: 60px; accent-color: #5af; }
#toolbar input[type="checkbox"] { accent-color: #5af; }

#hud {
  position: absolute; bottom: 8px; left: 8px;
  color: rgba(255,255,255,0.5); font-size: 11px; line-height: 1.5;
  pointer-events: none; user-select: none;
}
</style>
</head>
<body>
<div id="app">
  <canvas id="canvas"></canvas>
  <div id="toolbar">
    <button data-tool="select" class="active">🖱 Select</button>
    <button data-tool="rope">🪢 Rope</button>
    <button data-tool="cloth">🧵 Cloth</button>
    <button data-tool="softbody">🔵 Soft Body</button>
    <button data-tool="ball">⚽ Ball</button>
    <span class="sep">|</span>
    <button id="btn-pause">⏸ Pause</button>
    <button id="btn-step">⏭ Step</button>
    <button id="btn-reset">🔄 Reset</button>
    <span class="sep">|</span>
    <label>Gravity <input type="range" id="grav-slider" min="0" max="2000" value="980"></label>
    <label>Iterations <input type="range" id="iter-slider" min="1" max="20" value="6"></label>
    <span class="sep">|</span>
    <label><input type="checkbox" id="chk-stress"> Stress</label>
    <label><input type="checkbox" id="chk-points"> Points</label>
  </div>
  <div id="hud">
    <div id="fps">FPS: --</div>
    <div id="physTime">Phys: --ms</div>
    <div id="objCount">Objects: 0</div>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./physics":"src/physics.ts","./simulation":"src/simulation.ts","./renderer":"src/renderer.ts"},"src/physics.ts":{"./config":"src/config.ts"},"src/simulation.ts":{"./config":"src/config.ts","./physics":"src/physics.ts"},"src/renderer.ts":{"./config":"src/config.ts","./physics":"src/physics.ts","./simulation":"src/simulation.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 });
// Main entry: wire up canvas, input, toolbar, game loop
const config_1 = require("./config");
const physics_1 = require("./physics");
const simulation_1 = require("./simulation");
const renderer_1 = require("./renderer");
const canvas = document.getElementById('canvas');
const renderer = new renderer_1.Renderer(canvas);
const sim = new simulation_1.Simulation();
sim.setBounds(window.innerWidth, window.innerHeight);
// Add a static obstacle circle
sim.staticCircles.push({ cx: window.innerWidth / 2, cy: window.innerHeight * 0.5, r: 40 });
// Tool state
let currentTool = 'select';
let isDragging = false;
let isRightDrag = false;
let dragStartX = 0;
let dragStartY = 0;
let lastDragX = 0;
let lastDragY = 0;
let spawnPreview = null;
// FPS tracking
let lastTime = performance.now();
let fps = 0;
let frameCount = 0;
let fpsUpdateTime = 0;
let physStepTime = 0;
// HUD elements
const fpsEl = document.getElementById('fps');
const physTimeEl = document.getElementById('physTime');
const objCountEl = document.getElementById('objCount');
// Resize
function resize() {
    const w = window.innerWidth;
    const h = window.innerHeight;
    renderer.resize(w, h);
    sim.setBounds(w, h);
    // Update static circle position
    if (sim.staticCircles.length > 0) {
        sim.staticCircles[0].cx = w / 2;
        sim.staticCircles[0].cy = h * 0.5;
    }
}
resize();
window.addEventListener('resize', resize);
// Toolbar buttons
const toolbar = document.getElementById('toolbar');
toolbar.querySelectorAll('[data-tool]').forEach(btn => {
    btn.addEventListener('click', () => {
        toolbar.querySelectorAll('[data-tool]').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        currentTool = btn.dataset.tool || 'select';
        canvas.style.cursor = currentTool === 'select' ? 'default' : 'crosshair';
    });
});
// Pause / Step / Reset
document.getElementById('btn-pause').addEventListener('click', () => {
    sim.paused = !sim.paused;
    document.getElementById('btn-pause').textContent = sim.paused ? '▶ Play' : '⏸ Pause';
});
document.getElementById('btn-step').addEventListener('click', () => {
    sim.step(Number(gravSlider.value), Number(iterSlider.value));
    renderer.render(sim);
});
document.getElementById('btn-reset').addEventListener('click', () => {
    sim.removeObjects();
});
// Sliders & checkboxes
const gravSlider = document.getElementById('grav-slider');
const iterSlider = document.getElementById('iter-slider');
const chkStress = document.getElementById('chk-stress');
const chkPoints = document.getElementById('chk-points');
chkStress.addEventListener('change', () => { sim.showStress = chkStress.checked; });
chkPoints.addEventListener('change', () => { sim.showPoints = chkPoints.checked; });
// Keyboard
window.addEventListener('keydown', (e) => {
    if (e.code === 'Space') {
        e.preventDefault();
        sim.paused = !sim.paused;
        document.getElementById('btn-pause').textContent = sim.paused ? '▶ Play' : '⏸ Pause';
    }
    if (e.code === 'ArrowRight' && sim.paused) {
        e.preventDefault();
        sim.step(Number(gravSlider.value), Number(iterSlider.value));
        renderer.render(sim);
    }
});
// Mouse / pointer interaction
function getCanvasPos(e) {
    const rect = canvas.getBoundingClientRect();
    return { x: (e.clientX - rect.left) * (canvas.width / rect.width), y: (e.clientY - rect.top) * (canvas.height / rect.height) };
}
canvas.addEventListener('pointerdown', (e) => {
    const pos = getCanvasPos(e);
    isDragging = true;
    dragStartX = pos.x;
    dragStartY = pos.y;
    lastDragX = pos.x;
    lastDragY = pos.y;
    spawnPreview = null;
    if (e.button === 2 || e.getModifierState('ShiftKey')) {
        // Right click or shift: tear mode
        isRightDrag = true;
        return;
    }
    if (e.getModifierState('ControlKey')) {
        // Ctrl + click: toggle pin
        sim.togglePin(pos.x, pos.y, config_1.CONFIG.grabRadius);
        isDragging = false;
        return;
    }
    if (currentTool === 'select') {
        // Try to grab a point
        if (!sim.grabPoint(pos.x, pos.y, config_1.CONFIG.grabRadius)) {
            isDragging = false;
        }
        return;
    }
    // Start spawn drag
    spawnPreview = { x: pos.x, y: pos.y };
});
canvas.addEventListener('pointermove', (e) => {
    if (!isDragging)
        return;
    const pos = getCanvasPos(e);
    if (isRightDrag) {
        // Tear along drag path
        sim.tearConstraints(lastDragX, lastDragY, pos.x, pos.y, config_1.CONFIG.tearRadius);
        lastDragX = pos.x;
        lastDragY = pos.y;
        return;
    }
    if (sim.grabbedPoint) {
        // Set grabbed point position (velocity will be computed from position delta)
        sim.setGrabPosition(pos.x, pos.y);
        lastDragX = pos.x;
        lastDragY = pos.y;
        return;
    }
    if (spawnPreview && currentTool !== 'select') {
        spawnPreview = { x: pos.x, y: pos.y };
    }
});
canvas.addEventListener('pointerup', (e) => {
    if (isRightDrag) {
        isRightDrag = false;
        isDragging = false;
        return;
    }
    if (sim.grabbedPoint) {
        sim.releaseGrab();
        isDragging = false;
        return;
    }
    if (spawnPreview) {
        const pos = getCanvasPos(e);
        // Spawn object based on drag extent
        const dx = Math.abs(pos.x - dragStartX);
        const dy = Math.abs(pos.y - dragStartY);
        if (currentTool === 'rope') {
            const length = Math.sqrt(dx * dx + dy * dy) || config_1.CONFIG.ropeSegments * config_1.CONFIG.ropeSpacing;
            const segs = Math.max(4, Math.round(length / config_1.CONFIG.ropeSpacing));
            const angle = Math.atan2(dy, dx);
            const segLen = length / segs;
            const obj = (0, physics_1.createRope)(dragStartX, dragStartY, segs, segLen);
            // Orient the rope along the drag direction
            for (let i = 0; i < obj.points.length; i++) {
                const t = i / (obj.points.length - 1);
                obj.points[i].x = dragStartX + Math.cos(angle) * t * length;
                obj.points[i].xPrev = obj.points[i].x;
                obj.points[i].y = dragStartY + Math.sin(angle) * t * length;
                obj.points[i].yPrev = obj.points[i].y;
            }
            sim.addObject(obj);
        }
        else if (currentTool === 'cloth') {
            const w = Math.max(dx, 40);
            const h = Math.max(dy, 40);
            const cols = Math.max(4, Math.round(w / config_1.CONFIG.clothSpacing));
            const rows = Math.max(3, Math.round(h / config_1.CONFIG.clothSpacing));
            const obj = (0, physics_1.createCloth)(dragStartX, dragStartY, cols, rows, config_1.CONFIG.clothSpacing, 'corners');
            sim.addObject(obj);
        }
        else if (currentTool === 'softbody') {
            const r = Math.max(Math.sqrt(dx * dx + dy * dy), 30) / 2;
            const obj = (0, physics_1.createSoftBody)(dragStartX, dragStartY, r, config_1.CONFIG.softBodyPoints);
            sim.addObject(obj);
        }
        else if (currentTool === 'ball') {
            const obj = (0, physics_1.createBall)(dragStartX, dragStartY, config_1.CONFIG.ballRadius);
            sim.addObject(obj);
        }
        spawnPreview = null;
        isDragging = false;
    }
});
// Prevent context menu on canvas
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
// Expose sim on window for headless probing
window.sim = sim;
// Spawn initial objects (leave 30px margin from walls)
const w = window.innerWidth;
const h = window.innerHeight;
const margin = 30;
sim.addObject((0, physics_1.createRope)(w * 0.2, 50, config_1.CONFIG.ropeSegments, config_1.CONFIG.ropeSpacing));
sim.addObject((0, physics_1.createCloth)(w * 0.5 - 90, 70, config_1.CONFIG.clothCols, config_1.CONFIG.clothRows, config_1.CONFIG.clothSpacing, 'corners'));
sim.addObject((0, physics_1.createSoftBody)(w * 0.8, 150, config_1.CONFIG.softBodyRadius, config_1.CONFIG.softBodyPoints));
sim.addObject((0, physics_1.createBall)(w * 0.4, 120, config_1.CONFIG.ballRadius));
// Main loop
function loop(now) {
    // FPS calculation
    frameCount++;
    if (now - fpsUpdateTime > 500) {
        fps = Math.round(frameCount * 1000 / (now - fpsUpdateTime));
        frameCount = 0;
        fpsUpdateTime = now;
    }
    const dt = Math.min((now - lastTime) / 1000, 0.1);
    lastTime = now;
    const t0 = performance.now();
    sim.tick(dt, Number(gravSlider.value), Number(iterSlider.value));
    physStepTime = performance.now() - t0;
    renderer.render(sim);
    // Draw spawn preview
    if (spawnPreview && isDragging && currentTool !== 'select') {
        const e_pos = getCanvasPos({ clientX: 0, clientY: 0 });
        // We need the current pointer position — use spawnPreview as end
        const ctx = renderer['ctx'];
        ctx.setLineDash([4, 4]);
        ctx.strokeStyle = 'rgba(255,255,255,0.3)';
        ctx.lineWidth = 1;
        if (currentTool === 'rope') {
            ctx.beginPath();
            ctx.moveTo(dragStartX, dragStartY);
            ctx.lineTo(spawnPreview.x, spawnPreview.y);
            ctx.stroke();
        }
        else if (currentTool === 'cloth') {
            ctx.strokeRect(dragStartX, dragStartY, spawnPreview.x - dragStartX, spawnPreview.y - dragStartY);
        }
        else {
            ctx.beginPath();
            ctx.arc(dragStartX, dragStartY, Math.sqrt((spawnPreview.x - dragStartX) ** 2 + (spawnPreview.y - dragStartY) ** 2) / 2, 0, Math.PI * 2);
            ctx.stroke();
        }
        ctx.setLineDash([]);
    }
    // HUD
    fpsEl.textContent = `FPS: ${fps}`;
    physTimeEl.textContent = `Phys: ${physStepTime.toFixed(1)}ms`;
    objCountEl.textContent = `Objects: ${sim.objects.length} | Points: ${sim.getTotalPoints()}`;
    requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
};

// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONFIG = void 0;
// Tunable physics constants
exports.CONFIG = {
    // Physics
    gravity: 800,
    damping: 0.95,
    fixedDt: 1 / 60,
    maxFramesPerTick: 4,
    constraintIterations: 6,
    maxDelta: 1 / 20, // clamp frame delta to avoid explosion on tab switch
    // Interaction
    grabRadius: 16,
    tearRadius: 14,
    throwDamping: 0.9,
    // Objects
    ropeSegments: 20,
    ropeSpacing: 12,
    clothCols: 16,
    clothRows: 12,
    clothSpacing: 12,
    softBodyRadius: 50,
    softBodyPoints: 16,
    ballRadius: 20,
    // Rendering
    pointRadius: 2,
    clothPointRadius: 0,
};
};

// ── module: src/physics.ts ──
__mods["src/physics.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createRope = createRope;
exports.createCloth = createCloth;
exports.createSoftBody = createSoftBody;
exports.createBall = createBall;
// Build a rope
function createRope(x, y, segments, spacing) {
    const pts = [];
    const cons = [];
    for (let i = 0; i <= segments; i++) {
        pts.push({ x: x, y: y + i * spacing, xPrev: x, yPrev: y + i * spacing, pinned: i === 0, mass: 1 });
    }
    for (let i = 0; i < pts.length - 1; i++) {
        cons.push({ i, j: i + 1, restLength: spacing, broken: false });
    }
    return { type: 'rope', points: pts, constraints: cons, color: '#f95', fillColor: null };
}
// Build a cloth rectangle
function createCloth(x, y, cols, rows, spacing, pinMode) {
    const pts = [];
    const cons = [];
    for (let row = 0; row <= rows; row++) {
        for (let col = 0; col <= cols; col++) {
            const px = x + col * spacing;
            const py = y + row * spacing;
            pts.push({ x: px, y: py, xPrev: px, yPrev: py, pinned: false, mass: 1 });
        }
    }
    const pinRow = 0;
    for (let col = 0; col <= cols; col++) {
        const idx = pinRow * (cols + 1) + col;
        if (pinMode === 'edge' || col === 0 || col === cols) {
            pts[idx].pinned = true;
        }
    }
    // Structural constraints (horizontal + vertical)
    for (let row = 0; row <= rows; row++) {
        for (let col = 0; col < cols; col++) {
            const a = row * (cols + 1) + col;
            const b = a + 1;
            cons.push({ i: a, j: b, restLength: spacing, broken: false });
        }
    }
    for (let row = 0; row < rows; row++) {
        for (let col = 0; col <= cols; col++) {
            const a = row * (cols + 1) + col;
            const b = a + (cols + 1);
            cons.push({ i: a, j: b, restLength: spacing, broken: false });
        }
    }
    // Shear constraints (diagonal)
    for (let row = 0; row < rows; row++) {
        for (let col = 0; col < cols; col++) {
            const a = row * (cols + 1) + col;
            const b = a + cols + 2;
            cons.push({ i: a, j: b, restLength: spacing * Math.SQRT2, broken: false });
            const c = a + 1;
            const d = a + cols + 1;
            cons.push({ i: c, j: d, restLength: spacing * Math.SQRT2, broken: false });
        }
    }
    return { type: 'cloth', points: pts, constraints: cons, color: '#5af', fillColor: null };
}
// Build a soft body (circle of points with internal bracing)
function createSoftBody(cx, cy, radius, numPoints) {
    const pts = [];
    const cons = [];
    for (let i = 0; i < numPoints; i++) {
        const a = (i / numPoints) * Math.PI * 2 - Math.PI / 2;
        pts.push({
            x: cx + Math.cos(a) * radius,
            y: cy + Math.sin(a) * radius,
            xPrev: cx + Math.cos(a) * radius,
            yPrev: cy + Math.sin(a) * radius,
            pinned: false,
            mass: 1,
        });
    }
    // Center point
    pts.push({ x: cx, y: cy, xPrev: cx, yPrev: cy, pinned: false, mass: 1 });
    const centerIdx = numPoints;
    // Edge constraints
    for (let i = 0; i < numPoints; i++) {
        cons.push({ i, j: (i + 1) % numPoints, restLength: 2 * Math.sin(Math.PI / numPoints) * radius, broken: false });
        cons.push({ i, j: centerIdx, restLength: radius, broken: false });
    }
    // Cross bracing for stability
    for (let i = 0; i < numPoints; i++) {
        cons.push({ i, j: (i + 3) % numPoints, restLength: 2 * Math.sin(3 * Math.PI / numPoints) * radius, broken: false });
    }
    return { type: 'softbody', points: pts, constraints: cons, color: '#5f5', fillColor: 'rgba(85,255,85,0.12)' };
}
// Build a ball
function createBall(x, y, radius) {
    return {
        type: 'ball',
        points: [{ x, y, xPrev: x, yPrev: y, pinned: false, mass: 1 }],
        constraints: [],
        color: '#f5f',
        fillColor: null,
    };
}
};

// ── module: src/simulation.ts ──
__mods["src/simulation.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Simulation = void 0;
// Simulation engine: Verlet integration, constraint relaxation, collision
const config_1 = require("./config");
class Simulation {
    constructor() {
        this.objects = [];
        this.staticRects = [];
        this.staticCircles = [];
        this.paused = false;
        this.gravityOn = true;
        this.showStress = false;
        this.showPoints = false;
        this.accumulator = 0;
        this.grabbedPoint = null;
        // Floor (will be overridden by setBounds)
        this.staticRects.push({ x: -5000, y: 700, w: 10000, h: 20 });
    }
    setBounds(w, h) {
        this.staticRects = [
            { x: -5000, y: h - 10, w: 10000, h: 20 }, // floor
            { x: -10, y: -5000, w: 20, h: h + 5000 }, // left wall
            { x: w - 10, y: -5000, w: 20, h: h + 5000 }, // right wall
        ];
    }
    addObject(obj) {
        this.objects.push(obj);
    }
    removeObjects() {
        this.objects = [];
    }
    // Verlet integration step
    integrate(dt, gravity) {
        const dt2 = dt * dt;
        const damp = config_1.CONFIG.damping;
        for (const obj of this.objects) {
            for (const p of obj.points) {
                if (p.pinned)
                    continue;
                const vx = (p.x - p.xPrev) * damp;
                const vy = (p.y - p.yPrev) * damp;
                p.xPrev = p.x;
                p.yPrev = p.y;
                p.x += vx;
                p.y += vy + (this.gravityOn ? gravity : 0) * dt2;
            }
        }
    }
    // Constraint relaxation
    solveConstraints(iterations) {
        for (const obj of this.objects) {
            for (let iter = 0; iter < iterations; iter++) {
                for (const c of obj.constraints) {
                    if (c.broken)
                        continue;
                    const pi = obj.points[c.i];
                    const pj = obj.points[c.j];
                    const dx = pj.x - pi.x;
                    const dy = pj.y - pi.y;
                    let dist = Math.sqrt(dx * dx + dy * dy);
                    if (dist < 0.001) {
                        dist = 0.001;
                    }
                    const diff = (c.restLength - dist) / dist;
                    const mi = pi.pinned ? 0 : 1 / pi.mass;
                    const mj = pj.pinned ? 0 : 1 / pj.mass;
                    const total = mi + mj;
                    if (total < 0.001)
                        continue;
                    const wi = mi / total;
                    const wj = mj / total;
                    const ox = dx * diff;
                    const oy = dy * diff;
                    if (!pi.pinned) {
                        pi.x -= ox * wi;
                        pi.y -= oy * wi;
                    }
                    if (!pj.pinned) {
                        pj.x += ox * wj;
                        pj.y += oy * wj;
                    }
                }
            }
        }
    }
    // Collision with static geometry — always clamp outside of rects
    resolveCollisions() {
        const friction = 0.8;
        const restitution = 0.05;
        for (const obj of this.objects) {
            for (const p of obj.points) {
                if (p.pinned)
                    continue;
                const r = p.radius ?? 1.5;
                for (const rect of this.staticRects) {
                    const rx = rect.x - r;
                    const ry = rect.y - r;
                    const rw = rect.w + 2 * r;
                    const rh = rect.h + 2 * r;
                    if (p.x < rx || p.x > rx + rw || p.y < ry || p.y > ry + rh)
                        continue;
                    const dLeft = p.x - rx;
                    const dRight = (rx + rw) - p.x;
                    const dTop = p.y - ry;
                    const dBottom = (ry + rh) - p.y;
                    const minD = Math.min(dLeft, dRight, dTop, dBottom);
                    // Velocity before collision
                    const vx = p.x - p.xPrev;
                    const vy = p.y - p.yPrev;
                    // Sleep threshold: zero velocity when barely moving on a surface
                    const sleepThreshold = 0.3;
                    if (minD === dTop) {
                        p.y = ry;
                        if (vy > sleepThreshold) {
                            p.yPrev = p.y + vy * restitution;
                            p.xPrev = p.x - vx * friction;
                        }
                        else {
                            p.yPrev = p.y; // sleep
                            p.xPrev = p.x - vx * friction;
                        }
                    }
                    else if (minD === dBottom) {
                        p.y = ry + rh;
                        if (vy < -sleepThreshold) {
                            p.yPrev = p.y + vy * restitution;
                            p.xPrev = p.x - vx * friction;
                        }
                        else {
                            p.yPrev = p.y;
                            p.xPrev = p.x - vx * friction;
                        }
                    }
                    else if (minD === dLeft) {
                        p.x = rx;
                        if (vx > sleepThreshold) {
                            p.xPrev = p.x + vx * restitution;
                            p.yPrev = p.y - vy * friction;
                        }
                        else {
                            p.xPrev = p.x;
                            p.yPrev = p.y - vy * friction;
                        }
                    }
                    else {
                        p.x = rx + rw;
                        if (vx < -sleepThreshold) {
                            p.xPrev = p.x + vx * restitution;
                            p.yPrev = p.y - vy * friction;
                        }
                        else {
                            p.xPrev = p.x;
                            p.yPrev = p.y - vy * friction;
                        }
                    }
                }
                // Circle collision
                for (const circ of this.staticCircles) {
                    const dx = p.x - circ.cx;
                    const dy = p.y - circ.cy;
                    const dist = Math.sqrt(dx * dx + dy * dy);
                    const minDist = circ.r + r;
                    if (dist < minDist && dist > 0.001) {
                        const nx = dx / dist;
                        const ny = dy / dist;
                        p.x += nx * (minDist - dist);
                        p.y += ny * (minDist - dist);
                        const vx = p.x - p.xPrev;
                        const vy = p.y - p.yPrev;
                        const vn = vx * nx + vy * ny;
                        if (vn < 0) {
                            const tx = -ny;
                            const ty = nx;
                            const vt = vx * tx + vy * ty;
                            p.xPrev = p.x - (vn * restitution * nx + vt * friction * tx);
                            p.yPrev = p.y - (vn * restitution * ny + vt * friction * ty);
                        }
                    }
                }
            }
        }
    }
    // NaN guard + hard boundary enforcement (tunneling fallback)
    clampPoints() {
        const BOUND = 50000;
        const r = 1.5;
        for (const obj of this.objects) {
            for (const p of obj.points) {
                if (!isFinite(p.x) || !isFinite(p.y)) {
                    p.x = 0;
                    p.y = 0;
                }
                if (!isFinite(p.xPrev) || !isFinite(p.yPrev)) {
                    p.xPrev = p.x;
                    p.yPrev = p.y;
                }
                p.x = Math.max(-BOUND, Math.min(BOUND, p.x));
                p.y = Math.max(-BOUND, Math.min(BOUND, p.y));
                p.xPrev = Math.max(-BOUND, Math.min(BOUND, p.xPrev));
                p.yPrev = Math.max(-BOUND, Math.min(BOUND, p.yPrev));
                // Hard clamp against static rects (catch tunneling)
                if (p.pinned)
                    continue;
                for (const rect of this.staticRects) {
                    const rx1 = rect.x - r;
                    const ry1 = rect.y - r;
                    const rx2 = rect.x + rect.w + r;
                    const ry2 = rect.y + rect.h + r;
                    if (p.x >= rx1 && p.x <= rx2) {
                        if (p.y > ry2) {
                            p.y = ry2;
                            if (p.y - p.yPrev > 0.3) {
                                p.yPrev = p.y + (p.y - p.yPrev) * 0.05;
                            }
                            else {
                                p.yPrev = p.y;
                            }
                        }
                    }
                    if (p.y >= ry1 && p.y <= ry2) {
                        if (p.x < rx1) {
                            p.x = rx1;
                            p.xPrev = p.x;
                        }
                        if (p.x > rx2) {
                            p.x = rx2;
                            p.xPrev = p.x;
                        }
                    }
                }
            }
        }
    }
    step(gravity = config_1.CONFIG.gravity, iterations = config_1.CONFIG.constraintIterations) {
        const dt = config_1.CONFIG.fixedDt;
        this.integrate(dt, gravity);
        // Run collision after each constraint pass for stability
        for (let i = 0; i < iterations; i++) {
            this.solveConstraints(1);
            this.resolveCollisions();
        }
        this.clampPoints();
    }
    tick(deltaTime, gravity = config_1.CONFIG.gravity, iterations = config_1.CONFIG.constraintIterations) {
        if (this.paused)
            return;
        let dt = Math.min(deltaTime, config_1.CONFIG.maxDelta);
        this.accumulator += dt;
        let count = 0;
        while (this.accumulator >= config_1.CONFIG.fixedDt && count < config_1.CONFIG.maxFramesPerTick) {
            this.step(gravity, iterations);
            this.accumulator -= config_1.CONFIG.fixedDt;
            count++;
        }
        if (this.accumulator >= config_1.CONFIG.fixedDt) {
            this.accumulator = 0;
        }
    }
    findNearestPoint(mx, my, radius) {
        let best = null;
        for (const obj of this.objects) {
            for (let i = 0; i < obj.points.length; i++) {
                const p = obj.points[i];
                const dx = p.x - mx;
                const dy = p.y - my;
                const d = dx * dx + dy * dy;
                if (d < radius * radius) {
                    if (!best || d < best.dist) {
                        best = { obj, idx: i, dist: d };
                    }
                }
            }
        }
        return best ? { obj: best.obj, idx: best.idx } : null;
    }
    grabPoint(mx, my, radius) {
        const hit = this.findNearestPoint(mx, my, radius);
        if (hit) {
            this.grabbedPoint = hit;
            return true;
        }
        return false;
    }
    setGrabPosition(mx, my) {
        if (!this.grabbedPoint)
            return;
        const p = this.grabbedPoint.obj.points[this.grabbedPoint.idx];
        if (p.pinned)
            return;
        p.x = mx;
        p.y = my;
    }
    releaseGrab() {
        this.grabbedPoint = null;
    }
    tearConstraints(x1, y1, x2, y2, radius) {
        for (const obj of this.objects) {
            for (const c of obj.constraints) {
                if (c.broken)
                    continue;
                const pi = obj.points[c.i];
                const pj = obj.points[c.j];
                const mx = (pi.x + pj.x) / 2;
                const my = (pi.y + pj.y) / 2;
                const dx = x2 - x1;
                const dy = y2 - y1;
                const len2 = dx * dx + dy * dy;
                let t = 0;
                if (len2 > 0.001) {
                    t = ((mx - x1) * dx + (my - y1) * dy) / len2;
                    t = Math.max(0, Math.min(1, t));
                }
                const px = x1 + t * dx;
                const py = y1 + t * dy;
                const d2 = (mx - px) * (mx - px) + (my - py) * (my - py);
                if (d2 < radius * radius) {
                    c.broken = true;
                }
            }
        }
    }
    togglePin(mx, my, radius) {
        const hit = this.findNearestPoint(mx, my, radius);
        if (hit) {
            hit.obj.points[hit.idx].pinned = !hit.obj.points[hit.idx].pinned;
            return true;
        }
        return false;
    }
    getKineticEnergy() {
        let ke = 0;
        const dt = config_1.CONFIG.fixedDt;
        for (const obj of this.objects) {
            for (const p of obj.points) {
                const vx = (p.x - p.xPrev) / dt;
                const vy = (p.y - p.yPrev) / dt;
                ke += 0.5 * p.mass * (vx * vx + vy * vy);
            }
        }
        return ke;
    }
    getTotalPoints() {
        let n = 0;
        for (const obj of this.objects)
            n += obj.points.length;
        return n;
    }
    hasNaN() {
        for (const obj of this.objects) {
            for (const p of obj.points) {
                if (!isFinite(p.x) || !isFinite(p.y))
                    return true;
            }
        }
        return false;
    }
}
exports.Simulation = Simulation;
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
// Renderer: Canvas 2D drawing for the physics playground
const config_1 = require("./config");
class Renderer {
    constructor(canvas) {
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');
    }
    resize(w, h) {
        this.canvas.width = w;
        this.canvas.height = h;
    }
    render(sim) {
        const ctx = this.ctx;
        const w = this.canvas.width;
        const h = this.canvas.height;
        // Background with slight motion blur
        ctx.fillStyle = 'rgba(17, 17, 24, 0.92)';
        ctx.fillRect(0, 0, w, h);
        // Draw static geometry
        this.drawStatics(sim);
        // Draw objects
        for (const obj of sim.objects) {
            this.drawObject(ctx, obj, sim.showStress, sim.showPoints);
        }
        // Draw grab highlight
        if (sim['grabbedPoint']) {
            const gp = sim['grabbedPoint'];
            const p = gp.obj.points[gp.idx];
            ctx.beginPath();
            ctx.arc(p.x, p.y, config_1.CONFIG.grabRadius + 4, 0, Math.PI * 2);
            ctx.strokeStyle = 'rgba(255,255,100,0.6)';
            ctx.lineWidth = 2;
            ctx.stroke();
        }
    }
    drawStatics(sim) {
        const ctx = this.ctx;
        // Floor and walls
        ctx.fillStyle = 'rgba(50, 50, 70, 0.5)';
        for (const r of sim.staticRects) {
            ctx.fillRect(r.x, r.y, r.w, r.h);
        }
        // Floor/wall lines
        ctx.strokeStyle = 'rgba(100, 100, 130, 0.4)';
        ctx.lineWidth = 1;
        for (const r of sim.staticRects) {
            ctx.beginPath();
            ctx.moveTo(r.x, r.y);
            ctx.lineTo(r.x + r.w, r.y);
            ctx.stroke();
            ctx.beginPath();
            ctx.moveTo(r.x, r.y);
            ctx.lineTo(r.x, r.y + r.h);
            ctx.stroke();
        }
        // Static circles
        for (const c of sim.staticCircles) {
            ctx.fillStyle = 'rgba(50, 50, 70, 0.5)';
            ctx.beginPath();
            ctx.arc(c.cx, c.cy, c.r, 0, Math.PI * 2);
            ctx.fill();
            ctx.strokeStyle = 'rgba(100, 100, 130, 0.4)';
            ctx.stroke();
        }
    }
    drawObject(ctx, obj, showStress, showPoints) {
        const pts = obj.points;
        const cons = obj.constraints;
        // Draw filled shape for soft bodies
        if (obj.type === 'softbody' && obj.fillColor && pts.length > 2) {
            const centerIdx = pts.length - 1;
            ctx.beginPath();
            ctx.moveTo(pts[0].x, pts[0].y);
            for (let i = 1; i < centerIdx; i++) {
                ctx.lineTo(pts[i].x, pts[i].y);
            }
            ctx.closePath();
            ctx.fillStyle = obj.fillColor;
            ctx.fill();
        }
        // Draw cloth as filled mesh
        if (obj.type === 'cloth') {
            this.drawClothMesh(ctx, obj);
        }
        // Draw ball
        if (obj.type === 'ball' && pts.length === 1) {
            const r = config_1.CONFIG.ballRadius;
            ctx.beginPath();
            ctx.arc(pts[0].x, pts[0].y, r, 0, Math.PI * 2);
            ctx.fillStyle = 'rgba(255,85,255,0.2)';
            ctx.fill();
            ctx.strokeStyle = obj.color;
            ctx.lineWidth = 2;
            ctx.stroke();
            return;
        }
        // Draw constraints as lines
        for (const c of cons) {
            if (c.broken)
                continue;
            const pi = pts[c.i];
            const pj = pts[c.j];
            let color = obj.color;
            if (showStress) {
                const dx = pj.x - pi.x;
                const dy = pj.y - pi.y;
                const dist = Math.sqrt(dx * dx + dy * dy);
                if (c.restLength > 0.001) {
                    const stretch = dist / c.restLength;
                    const t = Math.min(1, Math.max(0, (stretch - 0.9) * 5));
                    color = this.stretchColor(t);
                }
            }
            ctx.beginPath();
            ctx.moveTo(pi.x, pi.y);
            ctx.lineTo(pj.x, pj.y);
            ctx.strokeStyle = color;
            ctx.lineWidth = obj.type === 'cloth' ? 0.8 : 1.5;
            ctx.stroke();
        }
        // Draw points
        if (showPoints) {
            for (const p of pts) {
                ctx.beginPath();
                ctx.arc(p.x, p.y, config_1.CONFIG.pointRadius, 0, Math.PI * 2);
                ctx.fillStyle = p.pinned ? '#ff0' : obj.color;
                ctx.fill();
            }
        }
    }
    drawClothMesh(ctx, obj) {
        const pts = obj.points;
        const cols = config_1.CONFIG.clothCols;
        const rows = config_1.CONFIG.clothRows;
        for (let row = 0; row < rows; row++) {
            for (let col = 0; col < cols; col++) {
                const a = row * (cols + 1) + col;
                const b = a + 1;
                const c = a + (cols + 1);
                const d = c + 1;
                // Fill quadrilateral
                ctx.beginPath();
                ctx.moveTo(pts[a].x, pts[a].y);
                ctx.lineTo(pts[b].x, pts[b].y);
                ctx.lineTo(pts[d].x, pts[d].y);
                ctx.lineTo(pts[c].x, pts[c].y);
                ctx.closePath();
                ctx.fillStyle = 'rgba(85,170,255,0.08)';
                ctx.fill();
            }
        }
    }
    stretchColor(t) {
        const r = Math.round(50 + 205 * t);
        const g = Math.round(220 - 180 * t);
        return `rgb(${r},${g},50)`;
    }
}
exports.Renderer = Renderer;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"ThinkingCap-Qwen3.6-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"c2155b86-1773-43d5-b721-c817a37e999c","tokensIn":3130814,"tokensOut":49377,"tokensTotal":3180191,"cost":0,"turns":82,"toolCalls":82,"failedToolCalls":1,"timestamp":"2026-08-03T21:35:39.497Z"} -->