Physics Playground
Qwen 3.6 27B MTP GGUF IQ4 NL · typescript

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 53,111 bytes · SHA-256 499c14396f41
<!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: #111; font-family: 'Segoe UI', system-ui, sans-serif; }
#canvas { display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%; cursor: crosshair; }
#toolbar {
position: fixed; top: 8px; left: 50%; transform: translateX(-50%);
display: flex; align-items: center; gap: 6px;
background: rgba(20,20,30,0.85); border: 1px solid rgba(100,100,140,0.4);
border-radius: 10px; padding: 6px 14px; z-index: 10;
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
}
#toolbar button {
background: rgba(60,60,100,0.6); color: #ccc; border: 1px solid rgba(100,100,160,0.3);
border-radius: 6px; padding: 5px 12px; font-size: 13px; cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
#toolbar button:hover { background: rgba(80,80,140,0.7); border-color: rgba(140,140,200,0.5); color: #fff; }
#toolbar button.active { background: rgba(100,120,200,0.5); border-color: rgba(160,160,240,0.6); color: #fff; }
#toolbar .sep { width: 1px; height: 24px; background: rgba(100,100,140,0.4); }
#toolbar label { color: #999; font-size: 12px; display: flex; align-items: center; gap: 4px; white-space: nowrap; }
#toolbar input[type="range"] { width: 70px; height: 4px; accent-color: #6688cc; }
#hud {
position: fixed; bottom: 8px; left: 8px;
display: flex; gap: 16px; font-size: 12px; color: rgba(180,180,220,0.8);
background: rgba(20,20,30,0.7); padding: 4px 12px; border-radius: 6px;
backdrop-filter: blur(4px); z-index: 10;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas><div id="toolbar">
<button id="btnRope" title="Rope (R)">⛓ Rope</button>
<button id="btnCloth" title="Cloth (C)">🧵 Cloth</button>
<button id="btnSoftBody" title="Soft Body (S)">🔵 Blob</button>
<button id="btnBall" title="Ball (B)">⚪ Ball</button>
<span class="sep"></span>
<button id="btnPause" title="Pause (Space)">⏸ Pause</button>
<button id="btnStep" title="Step (→)">⏭ Step</button>
<button id="btnReset" title="Reset All">🗑 Reset</button>
<span class="sep"></span>
<label>Grav: <input type="range" id="gravSlider" min="0" max="2000" value="980" step="10"></label>
<label>Iter: <input type="range" id="iterSlider" min="1" max="20" value="6" step="1"></label>
<label>Wind: <input type="range" id="windSlider" min="-500" max="500" value="0" step="10"></label>
</div>
<div id="hud">
<span id="hudFPS">FPS: --</span>
<span id="hudPoints">Points: 0</span>
<span id="hudConstraints">Constraints: 0</span>
<span id="hudTime">Physics: -- ms</span>
<span id="hudStatus"></span>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./simulation":"src/simulation.ts","./renderer":"src/renderer.ts","./input":"src/input.ts"},"src/simulation.ts":{"./config":"src/config.ts","./types":"src/types.ts"},"src/renderer.ts":{"./config":"src/config.ts","./simulation":"src/simulation.ts","./types":"src/types.ts"},"src/input.ts":{"./config":"src/config.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 });
const config_1 = require("./config");
const simulation_1 = require("./simulation");
const renderer_1 = require("./renderer");
const input_1 = require("./input");
// ── Main Entry ──
let sim;
let renderer;
let input;
// HUD elements
let hudFPS;
let hudPoints;
let hudConstraints;
let hudTime;
let hudStatus;
// FPS tracking
let frameCount = 0;
let fpsTimer = 0;
let displayFPS = 0;
// Time tracking
let lastTime = 0;
let physicsTimeSum = 0;
let physicsFrameCount = 0;
function init() {
const canvas = document.getElementById('canvas');
const app = document.body;
sim = new simulation_1.Simulation();
renderer = new renderer_1.Renderer(canvas);
input = new input_1.InputHandler(canvas, sim);
hudFPS = document.getElementById('hudFPS');
hudPoints = document.getElementById('hudPoints');
hudConstraints = document.getElementById('hudConstraints');
hudTime = document.getElementById('hudTime');
hudStatus = document.getElementById('hudStatus');
// Setup canvas size
const w = app.clientWidth;
const h = app.clientHeight;
sim.canvasW = w;
sim.canvasH = h;
renderer.resize(w, h);
sim.updateStaticCircles(w, h);
// Handle resize
window.addEventListener('resize', () => {
const w = app.clientWidth;
const h = app.clientHeight;
sim.canvasW = w;
sim.canvasH = h;
renderer.resize(w, h);
sim.updateStaticCircles(w, h);
});
// Toolbar button handlers
const btnRope = document.getElementById('btnRope');
const btnCloth = document.getElementById('btnCloth');
const btnSoftBody = document.getElementById('btnSoftBody');
const btnBall = document.getElementById('btnBall');
const btnPause = document.getElementById('btnPause');
const btnStep = document.getElementById('btnStep');
const btnReset = document.getElementById('btnReset');
const gravSlider = document.getElementById('gravSlider');
const iterSlider = document.getElementById('iterSlider');
const windSlider = document.getElementById('windSlider');
function setActiveButton(activeTool) {
btnRope.classList.toggle('active', activeTool === 'rope');
btnCloth.classList.toggle('active', activeTool === 'cloth');
btnSoftBody.classList.toggle('active', activeTool === 'softBody');
btnBall.classList.toggle('active', activeTool === 'ball');
}
btnRope.addEventListener('click', () => { input.selectTool('rope'); setActiveButton('rope'); });
btnCloth.addEventListener('click', () => { input.selectTool('cloth'); setActiveButton('cloth'); });
btnSoftBody.addEventListener('click', () => { input.selectTool('softBody'); setActiveButton('softBody'); });
btnBall.addEventListener('click', () => { input.selectTool('ball'); setActiveButton('ball'); });
btnPause.addEventListener('click', () => {
sim.paused = !sim.paused;
btnPause.textContent = sim.paused ? '▶ Play' : '⏸ Pause';
updateStatus();
});
btnStep.addEventListener('click', () => {
if (sim.paused) {
sim.step(config_1.CONFIG.FIXED_DT);
renderer.render(sim);
}
});
btnReset.addEventListener('click', () => {
sim.reset();
updateHUD();
});
gravSlider.addEventListener('input', () => {
sim.gravity = parseFloat(gravSlider.value);
});
iterSlider.addEventListener('input', () => {
sim.iterations = parseInt(iterSlider.value, 10);
});
windSlider.addEventListener('input', () => {
sim.wind = parseFloat(windSlider.value);
});
// Input callbacks
input.onTogglePause = (paused) => {
btnPause.textContent = paused ? '▶ Play' : '⏸ Pause';
updateStatus();
};
input.onStep = () => {
sim.step(config_1.CONFIG.FIXED_DT);
renderer.render(sim);
};
input.onToolSelect = (tool) => {
setActiveButton(tool);
};
// Spawn some default objects for demo
spawnDemoObjects();
// Expose on window for testing
window.sim = sim;
// Start loop
requestAnimationFrame(loop);
}
function spawnDemoObjects() {
const w = sim.canvasW;
const h = sim.canvasH;
// A rope from near the top
sim.spawnRope(w * 0.2, 30, config_1.CONFIG.ROPE_SEGMENTS, config_1.CONFIG.ROPE_SPACING, config_1.CONFIG.COLORS.rope);
// A cloth pinned at corners from near the top
sim.spawnCloth(w * 0.45, 50, config_1.CONFIG.CLOTH_COLS, config_1.CONFIG.CLOTH_ROWS, config_1.CONFIG.CLOTH_SPACING, config_1.CONFIG.COLORS.cloth, 'corners');
// A soft body
sim.spawnSoftBody(w * 0.75, 150, config_1.CONFIG.SOFT_BODY_RADIUS, config_1.CONFIG.SOFT_BODY_SEGMENTS, config_1.CONFIG.COLORS.softBody);
// A ball
sim.spawnBall(w * 0.1, 100, config_1.CONFIG.BALL_RADIUS, config_1.CONFIG.COLORS.ball);
}
function updateStatus() {
hudStatus.textContent = sim.paused ? 'PAUSED' : '';
}
function updateHUD() {
hudPoints.textContent = `Points: ${sim.totalPoints()}`;
hudConstraints.textContent = `Constraints: ${sim.totalConstraints()}`;
}
function loop(timestamp) {
// FPS calculation
if (!lastTime)
lastTime = timestamp;
const deltaMs = timestamp - lastTime;
lastTime = timestamp;
frameCount++;
fpsTimer += deltaMs;
if (fpsTimer >= 1000) {
displayFPS = Math.round(frameCount * 1000 / fpsTimer);
hudFPS.textContent = `FPS: ${displayFPS}`;
frameCount = 0;
fpsTimer = 0;
// Update physics time display
if (physicsFrameCount > 0) {
const avgMs = physicsTimeSum / physicsFrameCount;
hudTime.textContent = `Physics: ${avgMs.toFixed(1)} ms`;
}
physicsTimeSum = 0;
physicsFrameCount = 0;
updateHUD();
}
if (!sim.paused) {
// Fixed timestep accumulator with max delta clamp
const delta = Math.min(deltaMs / 1000, config_1.CONFIG.MAX_FRAME_DELTA);
sim.accumulator += delta;
let steps = 0;
const maxSteps = 10; // prevent spiral of death
const startPhysics = performance.now();
while (sim.accumulator >= config_1.CONFIG.FIXED_DT && steps < maxSteps) {
sim.step(config_1.CONFIG.FIXED_DT);
sim.accumulator -= config_1.CONFIG.FIXED_DT;
steps++;
}
// Drain remaining accumulator to prevent spiral
if (steps >= maxSteps) {
sim.accumulator = 0;
}
const physicsMs = performance.now() - startPhysics;
physicsTimeSum += physicsMs;
physicsFrameCount++;
}
// Render
renderer.render(sim);
// Draw preview lines for rope/cloth tool
if (input.mouseDown && input.tool === 'rope') {
const ctx = renderer.ctx;
ctx.beginPath();
ctx.moveTo(input.mouseStartX, input.mouseStartY);
ctx.lineTo(input.mouseX, input.mouseY);
ctx.strokeStyle = 'rgba(79, 195, 247, 0.5)';
ctx.lineWidth = 3;
ctx.setLineDash([5, 5]);
ctx.stroke();
ctx.setLineDash([]);
}
else if (input.mouseDown && input.tool === 'cloth') {
const ctx = renderer.ctx;
const cols = config_1.CONFIG.CLOTH_COLS;
const rows = config_1.CONFIG.CLOTH_ROWS;
const spacing = config_1.CONFIG.CLOTH_SPACING;
const w = cols * spacing;
const h = rows * spacing;
ctx.strokeStyle = 'rgba(129, 199, 132, 0.4)';
ctx.lineWidth = 1;
ctx.strokeRect(input.mouseStartX, input.mouseStartY, w, h);
}
requestAnimationFrame(loop);
}
// Start
init();
};
// ── 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 constants
exports.CONFIG = {
// Physics
FIXED_DT: 1 / 60,
MAX_FRAME_DELTA: 0.1, // clamp to 100ms to prevent explosion on tab switch
DAMPING: 0.99,
GRAVITY: 980,
WIND: 0,
ITERATIONS: 6,
// Collision
FLOOR_Y_OFFSET: 0, // floor is at canvas height - this
WALL_INSET: 2,
FRICTION: 0.8,
RESTITUTION: 0.4,
// Interaction
GRAB_RADIUS: 20,
TEAR_RADIUS: 15,
THROW_SCALE: 3, // multiply release velocity for "throw"
// Spawn sizes
ROPE_SEGMENTS: 20,
ROPE_SPACING: 12,
CLOTH_COLS: 18,
CLOTH_ROWS: 12,
CLOTH_SPACING: 14,
SOFT_BODY_RINGS: 1,
SOFT_BODY_SEGMENTS: 14,
SOFT_BODY_RADIUS: 40,
BALL_RADIUS: 15,
// Rendering
POINT_RADIUS: 3,
SHOW_POINTS: false,
SHOW_STRESS: false,
MOTION_BLUR: 0.15, // 0 = no blur, 1 = full trail
// Colors per object type
COLORS: {
rope: '#4fc3f7',
cloth: '#81c784',
softBody: '#e57373',
ball: '#ffb74d',
},
// Static geometry
STATIC_CIRCLES: [
{ x: 0.5, y: 0.42, r: 45 }, // under the cloth
{ x: 0.8, y: 0.82, r: 35 }, // lower right
],
};
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
};
// ── module: src/simulation.ts ──
__mods["src/simulation.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Simulation = void 0;
const config_1 = require("./config");
// ── Simulation State ──
class Simulation {
constructor() {
this.points = [];
this.constraints = [];
this.objects = [];
this.grabPoint = null;
this.grabMouseX = 0;
this.grabMouseY = 0;
this.isDragging = false;
this.dragStartX = 0;
this.dragStartY = 0;
this.paused = false;
this.accumulator = 0;
this.canvasW = 800;
this.canvasH = 600;
this.gravity = config_1.CONFIG.GRAVITY;
this.wind = 0;
this.iterations = config_1.CONFIG.ITERATIONS;
this.showStress = false;
// For exposed testing
this.staticCircles = [];
// Nothing — populated at runtime
}
// ── Point/Constraint Helpers ──
addPoint(x, y, pinned = false, radius = 0, obj = -1) {
const idx = this.points.length;
this.points.push({
x, y,
px: x, py: y,
pinned,
radius,
obj,
});
return idx;
}
addConstraint(a, b, length, obj) {
const idx = this.constraints.length;
this.constraints.push({ a, b, length, obj, torn: false });
return idx;
}
// Safe distance helper (avoids sqrt(0))
dist(x1, y1, x2, y2) {
const dx = x2 - x1, dy = y2 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
// ── Object Spawning ──
spawnRope(x, y, segments, spacing, color) {
const objIdx = this.objects.length;
const obj = {
type: 'rope', color,
pointStart: this.points.length, pointCount: 0,
constraintStart: this.constraints.length, constraintCount: 0,
alive: true,
};
// Add all points first
for (let i = 0; i < segments + 1; i++) {
this.addPoint(x, y + i * spacing, i === 0, 0, objIdx); // pin top
}
// Then add constraints between consecutive points
for (let i = 0; i < segments; i++) {
this.addConstraint(this.points.length - segments - 1 + i, this.points.length - segments + i, spacing, objIdx);
}
obj.pointCount = segments + 1;
obj.constraintCount = segments;
this.objects.push(obj);
}
spawnCloth(x, y, cols, rows, spacing, color, pinPattern) {
const objIdx = this.objects.length;
const obj = {
type: 'cloth', color,
pointStart: this.points.length, pointCount: 0,
constraintStart: this.constraints.length, constraintCount: 0,
alive: true,
};
// Create grid
const grid = [];
for (let row = 0; row < rows; row++) {
grid[row] = [];
for (let col = 0; col < cols; col++) {
const px = x + col * spacing;
const py = y + row * spacing;
let pinned = false;
if (pinPattern === 'full' && row === 0) {
pinned = true;
}
else if (pinPattern === 'corners' && row === 0 && (col === 0 || col === cols - 1)) {
pinned = true;
}
grid[row][col] = this.addPoint(px, py, pinned, 0, objIdx);
}
}
obj.pointCount = cols * rows;
// Structural + shear constraints
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const idx = grid[row][col];
// Right neighbor
if (col + 1 < cols) {
this.addConstraint(idx, grid[row][col + 1], spacing, objIdx);
}
// Bottom neighbor
if (row + 1 < rows) {
this.addConstraint(idx, grid[row + 1][col], spacing, objIdx);
}
// Shear: bottom-right diagonal
if (row + 1 < rows && col + 1 < cols) {
this.addConstraint(idx, grid[row + 1][col + 1], spacing * Math.SQRT2, objIdx);
}
}
}
obj.constraintCount = this.constraints.length - obj.constraintStart;
this.objects.push(obj);
}
spawnSoftBody(x, y, radius, segments, color) {
const objIdx = this.objects.length;
const obj = {
type: 'softBody', color,
pointStart: this.points.length, pointCount: 0,
constraintStart: this.constraints.length, constraintCount: 0,
alive: true,
};
// Create ring of points
const ring = [];
for (let i = 0; i < segments; i++) {
const angle = (i / segments) * Math.PI * 2 - Math.PI / 2;
const px = x + Math.cos(angle) * radius;
const py = y + Math.sin(angle) * radius;
ring.push(this.addPoint(px, py, false, 0, objIdx));
}
// Center point
const center = this.addPoint(x, y, false, 0, objIdx);
obj.pointCount = segments + 1;
// Edge constraints (ring)
for (let i = 0; i < segments; i++) {
const next = (i + 1) % segments;
this.addConstraint(ring[i], ring[next], radius * 2 * Math.sin(Math.PI / segments), objIdx);
}
// Internal braces: center to each ring point
for (let i = 0; i < segments; i++) {
this.addConstraint(center, ring[i], radius, objIdx);
}
// Cross braces for extra stiffness
for (let i = 0; i < segments; i++) {
const opp = (i + Math.floor(segments / 2)) % segments;
this.addConstraint(ring[i], ring[opp], radius * 2, objIdx);
}
obj.constraintCount = this.constraints.length - obj.constraintStart;
this.objects.push(obj);
}
spawnBall(x, y, radius, color) {
const objIdx = this.objects.length;
const obj = {
type: 'ball', color,
pointStart: this.points.length, pointCount: 1,
constraintStart: this.constraints.length, constraintCount: 0,
alive: true,
};
this.addPoint(x, y, false, radius, objIdx);
this.objects.push(obj);
}
// ── Physics Step ──
updateStaticCircles(w, h) {
this.staticCircles = config_1.CONFIG.STATIC_CIRCLES.map(c => ({
x: c.x * w,
y: (c.y) * h,
r: c.r,
}));
}
step(dt) {
const damping = config_1.CONFIG.DAMPING;
const floorY = this.canvasH - config_1.CONFIG.FLOOR_Y_OFFSET;
const wallLeft = config_1.CONFIG.WALL_INSET;
const wallRight = this.canvasW - config_1.CONFIG.WALL_INSET;
// 1. Verlet Integration
for (let i = 0; i < this.points.length; i++) {
const p = this.points[i];
if (p.pinned)
continue;
if (this.grabPoint === i)
continue; // grabbed points follow mouse
const vx = (p.x - p.px) * damping;
const vy = (p.y - p.py) * damping;
p.px = p.x;
p.py = p.y;
p.x += vx;
p.y += vy + this.gravity * dt * dt;
// Wind
p.x += this.wind * dt * dt;
}
// 2. Constraint Relaxation (multiple iterations)
for (let iter = 0; iter < this.iterations; iter++) {
// Distance constraints
for (let i = 0; i < this.constraints.length; i++) {
const c = this.constraints[i];
if (c.torn)
continue;
const pa = this.points[c.a];
const pb = this.points[c.b];
const dx = pb.x - pa.x;
const dy = pb.y - pa.y;
const distSq = dx * dx + dy * dy;
// Guard against zero-length / coincident
if (distSq < 0.0001)
continue;
const dist = Math.sqrt(distSq);
const diff = (c.length - dist) / dist;
// Guard against division by zero in mass calc
const totalMass = (pa.pinned ? 0 : 1) + (pb.pinned ? 0 : 1);
if (totalMass < 0.001)
continue;
const halfDiff = diff * 0.5;
if (!pa.pinned) {
pa.x -= dx * halfDiff;
pa.y -= dy * halfDiff;
}
if (!pb.pinned) {
pb.x += dx * halfDiff;
pb.y += dy * halfDiff;
}
}
// Wall/Floor collisions
for (let i = 0; i < this.points.length; i++) {
const p = this.points[i];
if (p.pinned)
continue;
// Floor
if (p.y > floorY) {
const vy = p.y - p.py;
p.y = floorY;
p.py = p.y + vy * config_1.CONFIG.RESTITUTION;
p.px = p.x - (p.x - p.px) * config_1.CONFIG.FRICTION;
}
// Ceiling
if (p.y < -p.radius) {
p.y = -p.radius;
p.py = p.y + (p.y - p.py) * config_1.CONFIG.RESTITUTION;
}
// Left wall
if (p.x < wallLeft) {
const vx = p.x - p.px;
p.x = wallLeft;
p.px = p.x + vx * config_1.CONFIG.RESTITUTION;
p.py = p.y - (p.y - p.py) * config_1.CONFIG.FRICTION;
}
// Right wall
if (p.x > wallRight) {
const vx = p.x - p.px;
p.x = wallRight;
p.px = p.x + vx * config_1.CONFIG.RESTITUTION;
p.py = p.y - (p.y - p.py) * config_1.CONFIG.FRICTION;
}
// Static circle collisions
for (let s = 0; s < this.staticCircles.length; s++) {
const sc = this.staticCircles[s];
const dx = p.x - sc.x;
const dy = p.y - sc.y;
const distSq = dx * dx + dy * dy;
const minDist = sc.r + p.radius;
if (distSq < minDist * minDist && distSq > 0.0001) {
const dist = Math.sqrt(distSq);
const nx = dx / dist;
const ny = dy / dist;
const overlap = minDist - dist;
p.x += nx * overlap;
p.y += ny * overlap;
// Friction: damp tangential velocity
const vx = p.x - p.px;
const vy = p.y - p.py;
const dot = vx * nx + vy * ny;
const tx = vx - dot * nx;
const ty = vy - dot * ny;
p.px = p.x - (dot * nx + tx * config_1.CONFIG.FRICTION);
p.py = p.y - (dot * ny + ty * config_1.CONFIG.FRICTION);
}
}
}
// Point-to-point collisions (for balls mainly)
for (let i = 0; i < this.points.length; i++) {
const pi = this.points[i];
if (pi.pinned || pi.radius <= 0)
continue;
for (let j = i + 1; j < this.points.length; j++) {
const pj = this.points[j];
if (pj.pinned || pj.radius <= 0)
continue;
if (pi.obj === pj.obj)
continue; // skip same object
const dx = pj.x - pi.x;
const dy = pj.y - pi.y;
const distSq = dx * dx + dy * dy;
const minDist = pi.radius + pj.radius;
if (distSq < minDist * minDist && distSq > 0.0001) {
const dist = Math.sqrt(distSq);
const nx = dx / dist;
const ny = dy / dist;
const overlap = (minDist - dist) * 0.5;
if (!pi.pinned) {
pi.x -= nx * overlap;
pi.y -= ny * overlap;
}
if (!pj.pinned) {
pj.x += nx * overlap;
pj.y += ny * overlap;
}
}
}
}
}
// NaN guard: reset any NaN points
for (let i = 0; i < this.points.length; i++) {
const p = this.points[i];
if (Number.isNaN(p.x) || Number.isNaN(p.y) || Number.isNaN(p.px) || Number.isNaN(p.py)) {
p.x = 0;
p.y = 0;
p.px = 0;
p.py = 0;
}
}
}
// ── Grab / Interaction ──
findNearestPoint(mx, my, radius) {
let bestIdx = -1;
let bestDist = radius * radius;
for (let i = 0; i < this.points.length; i++) {
const p = this.points[i];
const dx = p.x - mx;
const dy = p.y - my;
const d2 = dx * dx + dy * dy;
if (d2 < bestDist) {
bestDist = d2;
bestIdx = i;
}
}
return bestIdx >= 0 ? bestIdx : null;
}
grabPointAt(mx, my) {
const idx = this.findNearestPoint(mx, my, config_1.CONFIG.GRAB_RADIUS);
if (idx !== null && idx >= 0) {
this.grabPoint = idx;
this.isDragging = true;
this.grabMouseX = mx;
this.grabMouseY = my;
this.dragStartX = mx;
this.dragStartY = my;
return true;
}
return false;
}
releasePoint() {
if (this.grabPoint !== null) {
// Apply throw velocity
const p = this.points[this.grabPoint];
const throwX = this.grabMouseX;
const throwY = this.grabMouseY;
p.px = p.x - (throwX - p.x) * config_1.CONFIG.THROW_SCALE;
p.py = p.y - (throwY - p.y) * config_1.CONFIG.THROW_SCALE;
}
this.grabPoint = null;
this.isDragging = false;
}
// Update grabbed point position during drag
updateGrab(mx, my) {
if (this.grabPoint !== null) {
const p = this.points[this.grabPoint];
p.x = mx;
p.y = my;
}
}
// Tear constraints along a line segment
tearAlongLine(x1, y1, x2, y2) {
const tearR = config_1.CONFIG.TEAR_RADIUS;
for (let i = 0; i < this.constraints.length; i++) {
const c = this.constraints[i];
if (c.torn)
continue;
const pa = this.points[c.a];
const pb = this.points[c.b];
// Distance from segment to constraint midpoint
const mx = (pa.x + pb.x) * 0.5;
const my = (pa.y + pb.y) * 0.5;
// Simple: check if midpoint is near the drag line
const dx = mx - x1;
const dy = my - y1;
const d2 = dx * dx + dy * dy;
if (d2 < tearR * tearR) {
c.torn = true;
}
// Also check distance from line segment to both endpoints
const distToA = this.distPointToSegment(pa.x, pa.y, x1, y1, x2, y2);
const distToB = this.distPointToSegment(pb.x, pb.y, x1, y1, x2, y2);
if (distToA < tearR || distToB < tearR) {
c.torn = true;
}
}
}
distPointToSegment(px, py, x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
const lenSq = dx * dx + dy * dy;
if (lenSq < 0.0001)
return this.dist(px, py, x1, y1);
let t = ((px - x1) * dx + (py - y1) * dy) / lenSq;
t = Math.max(0, Math.min(1, t));
const projX = x1 + t * dx;
const projY = y1 + t * dy;
return this.dist(px, py, projX, projY);
}
togglePin(mx, my) {
const idx = this.findNearestPoint(mx, my, config_1.CONFIG.GRAB_RADIUS);
if (idx !== null && idx >= 0) {
this.points[idx].pinned = !this.points[idx].pinned;
}
}
// ── Utilities ──
getKineticEnergy() {
let energy = 0;
for (let i = 0; i < this.points.length; i++) {
const p = this.points[i];
const vx = p.x - p.px;
const vy = p.y - p.py;
energy += vx * vx + vy * vy;
}
return energy;
}
hasNaN() {
for (let i = 0; i < this.points.length; i++) {
const p = this.points[i];
if (Number.isNaN(p.x) || Number.isNaN(p.y))
return true;
}
return false;
}
outOfBounds(bound) {
for (let i = 0; i < this.points.length; i++) {
const p = this.points[i];
if (Math.abs(p.x) > bound || Math.abs(p.y) > bound)
return true;
}
return false;
}
reset() {
this.points = [];
this.constraints = [];
this.objects = [];
this.grabPoint = null;
this.isDragging = false;
this.accumulator = 0;
}
totalPoints() {
return this.points.length;
}
totalConstraints() {
return this.constraints.length;
}
}
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;
const config_1 = require("./config");
// ── Renderer ──
class Renderer {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
}
resize(w, h) {
const dpr = window.devicePixelRatio || 1;
this.canvas.width = w * dpr;
this.canvas.height = h * dpr;
this.canvas.style.width = w + 'px';
this.canvas.style.height = h + 'px';
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
render(sim) {
const ctx = this.ctx;
const w = sim.canvasW;
const h = sim.canvasH;
// Motion blur: semi-transparent overlay
if (config_1.CONFIG.MOTION_BLUR > 0 && !sim.paused) {
ctx.fillStyle = `rgba(17, 17, 25, ${1 - config_1.CONFIG.MOTION_BLUR})`;
ctx.fillRect(0, 0, w, h);
}
else {
// Full clear
ctx.fillStyle = '#111119';
ctx.fillRect(0, 0, w, h);
}
// Draw walls/floor
this.drawBounds(ctx, w, h);
// Draw static circles
for (const sc of sim.staticCircles) {
ctx.beginPath();
ctx.arc(sc.x, sc.y, sc.r, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(60, 60, 80, 0.6)';
ctx.fill();
ctx.strokeStyle = 'rgba(100, 100, 130, 0.5)';
ctx.lineWidth = 1.5;
ctx.stroke();
}
// Render each object
for (const obj of sim.objects) {
if (!obj.alive)
continue;
switch (obj.type) {
case 'rope':
this.renderRope(sim, obj);
break;
case 'cloth':
this.renderCloth(sim, obj);
break;
case 'softBody':
this.renderSoftBody(sim, obj);
break;
case 'ball':
this.renderBall(sim, obj);
break;
}
}
// Render points
if (config_1.CONFIG.SHOW_POINTS) {
this.renderPoints(sim);
}
// Draw grab highlight
if (sim.grabPoint !== null) {
const gp = sim.points[sim.grabPoint];
ctx.beginPath();
ctx.arc(gp.x, gp.y, config_1.CONFIG.GRAB_RADIUS, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.lineWidth = 2;
ctx.stroke();
}
}
drawBounds(ctx, w, h) {
const inset = config_1.CONFIG.WALL_INSET;
ctx.strokeStyle = 'rgba(80, 80, 110, 0.4)';
ctx.lineWidth = 2;
ctx.strokeRect(inset, inset, w - inset * 2, h - inset * 2);
// Floor highlight
ctx.fillStyle = 'rgba(60, 60, 90, 0.15)';
ctx.fillRect(inset, h - 4, w - inset * 2, 4);
}
renderRope(sim, obj) {
const ctx = this.ctx;
const stressVis = sim.showStress;
// Collect connected chains (handle torn segments)
const chains = [];
const constraintTorn = [];
for (let i = 0; i < obj.constraintCount; i++) {
constraintTorn[i] = sim.constraints[obj.constraintStart + i].torn;
}
// Build chains of connected points
let chain = [obj.pointStart];
for (let i = 0; i < obj.constraintCount; i++) {
if (!constraintTorn[i]) {
chain.push(obj.pointStart + i + 1);
}
else {
if (chain.length > 1)
chains.push(chain);
chain = [obj.pointStart + i + 1];
}
}
if (chain.length > 1)
chains.push(chain);
// Draw each chain as a smooth curve
for (const ch of chains) {
ctx.beginPath();
for (let j = 0; j < ch.length; j++) {
const p = sim.points[ch[j]];
if (j === 0)
ctx.moveTo(p.x, p.y);
else
ctx.lineTo(p.x, p.y);
}
if (stressVis) {
// Color by stress along the chain
const gradient = ctx.createLinearGradient(sim.points[ch[0]].x, sim.points[ch[0]].y, sim.points[ch[ch.length - 1]].x, sim.points[ch[ch.length - 1]].y);
gradient.addColorStop(0, '#4caf50');
gradient.addColorStop(1, '#f44336');
ctx.strokeStyle = gradient;
}
else {
ctx.strokeStyle = obj.color;
}
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.stroke();
// Draw dots at joints for visual clarity
for (let j = 0; j < ch.length; j++) {
const p = sim.points[ch[j]];
ctx.beginPath();
ctx.arc(p.x, p.y, 2.5, 0, Math.PI * 2);
ctx.fillStyle = obj.color;
ctx.fill();
}
// Pin indicator
const firstP = sim.points[ch[0]];
if (firstP.pinned) {
ctx.beginPath();
ctx.arc(firstP.x, firstP.y, 5, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.fill();
ctx.strokeStyle = obj.color;
ctx.lineWidth = 2;
ctx.stroke();
}
}
}
renderCloth(sim, obj) {
const ctx = this.ctx;
const cols = config_1.CONFIG.CLOTH_COLS;
const rows = config_1.CONFIG.CLOTH_ROWS;
// Build grid to render
const grid = [];
const pStart = obj.pointStart;
for (let r = 0; r < rows; r++) {
grid[r] = [];
for (let c = 0; c < cols; c++) {
const idx = pStart + r * cols + c;
grid[r][c] = sim.points[idx];
}
}
// Build constraint lookup: map "a_b" → torn status (either direction)
const tornLookup = new Map();
for (let ci = 0; ci < sim.constraints.length; ci++) {
const cc = sim.constraints[ci];
const key1 = `${cc.a}_${cc.b}`;
const key2 = `${cc.b}_${cc.a}`;
tornLookup.set(key1, cc.torn);
tornLookup.set(key2, cc.torn);
}
const isTorn = (a, b) => {
return tornLookup.get(`${a}_${b}`) === true;
};
// Draw filled quads as triangles (2 per quad)
for (let r = 0; r < rows - 1; r++) {
for (let c = 0; c < cols - 1; c++) {
const p00 = grid[r][c];
const p10 = grid[r][c + 1];
const p01 = grid[r + 1][c];
const p11 = grid[r + 1][c + 1];
const idx00 = pStart + r * cols + c;
const idx10 = pStart + r * cols + c + 1;
const idx01 = pStart + (r + 1) * cols + c;
const idx11 = pStart + (r + 1) * cols + c + 1;
const rightTorn = isTorn(idx00, idx10);
const leftTorn = isTorn(idx01, idx11);
const vertLTorn = isTorn(idx00, idx01);
const vertRTorn = isTorn(idx10, idx11);
// Draw triangle 1: top-left, top-right, bottom-left
if (!rightTorn && !vertLTorn) {
ctx.beginPath();
ctx.moveTo(p00.x, p00.y);
ctx.lineTo(p10.x, p10.y);
ctx.lineTo(p01.x, p01.y);
ctx.closePath();
ctx.fillStyle = this.alphaColor(obj.color, 0.2);
ctx.fill();
}
// Draw triangle 2: top-right, bottom-right, bottom-left
if (!vertRTorn && !leftTorn) {
ctx.beginPath();
ctx.moveTo(p10.x, p10.y);
ctx.lineTo(p11.x, p11.y);
ctx.lineTo(p01.x, p01.y);
ctx.closePath();
ctx.fillStyle = this.alphaColor(obj.color, 0.2);
ctx.fill();
}
}
}
// Draw grid lines (more visible structure)
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols - 1; c++) {
const idx = pStart + r * cols + c;
const idxR = pStart + r * cols + c + 1;
if (!isTorn(idx, idxR)) {
ctx.beginPath();
ctx.moveTo(grid[r][c].x, grid[r][c].y);
ctx.lineTo(grid[r][c + 1].x, grid[r][c + 1].y);
ctx.strokeStyle = this.alphaColor(obj.color, 0.35);
ctx.lineWidth = 0.8;
ctx.stroke();
}
}
}
for (let r = 0; r < rows - 1; r++) {
for (let c = 0; c < cols; c++) {
const idx = pStart + r * cols + c;
const idxB = pStart + (r + 1) * cols + c;
if (!isTorn(idx, idxB)) {
ctx.beginPath();
ctx.moveTo(grid[r][c].x, grid[r][c].y);
ctx.lineTo(grid[r + 1][c].x, grid[r + 1][c].y);
ctx.strokeStyle = this.alphaColor(obj.color, 0.35);
ctx.lineWidth = 0.8;
ctx.stroke();
}
}
}
// Draw pin indicators
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const p = grid[r][c];
if (p.pinned) {
ctx.beginPath();
ctx.arc(p.x, p.y, 5, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.fill();
ctx.strokeStyle = this.alphaColor(obj.color, 0.8);
ctx.lineWidth = 2;
ctx.stroke();
}
}
}
}
renderSoftBody(sim, obj) {
const ctx = this.ctx;
const segments = obj.pointCount - 1; // center + ring
const pStart = obj.pointStart;
// Collect ring points in order
const ringPts = [];
for (let i = 0; i < segments; i++) {
ringPts.push(sim.points[pStart + i]);
}
// Find torn edges to split into chains
const tornEdges = [];
for (let i = 0; i < segments; i++) {
const next = (i + 1) % segments;
const ci = obj.constraintStart + i;
tornEdges[i] = sim.constraints[ci].torn;
}
// Build chains of connected (non-torn) points
const chains = [];
for (let i = 0; i < segments; i++) {
let chain = [i];
while (tornEdges[(chain[chain.length - 1]) % segments] && chain.length < segments) {
const next = (chain[chain.length - 1] + 1) % segments;
if (next === chain[0] && chain.length > 1)
break;
chain.push(next);
}
chains.push(chain);
// Skip already added
for (let j = 1; j < chain.length; j++) {
// We'll deduplicate by skipping chains that start with already-added indices
}
}
// Simpler approach: just draw what we can
// Draw filled polygon from all ring points (even with tears, it looks fine for soft body)
ctx.beginPath();
for (let i = 0; i < segments; i++) {
const p = ringPts[i];
if (i === 0)
ctx.moveTo(p.x, p.y);
else
ctx.lineTo(p.x, p.y);
}
ctx.closePath();
ctx.fillStyle = this.alphaColor(obj.color, 0.3);
ctx.fill();
ctx.strokeStyle = obj.color;
ctx.lineWidth = 2;
ctx.stroke();
// Draw internal lines (center to ring)
const center = sim.points[pStart + segments];
for (let i = 0; i < segments; i++) {
const ci = obj.constraintStart + segments + i; // center-to-ring constraints
if (ci < sim.constraints.length && !sim.constraints[ci].torn) {
const p = ringPts[i];
ctx.beginPath();
ctx.moveTo(center.x, center.y);
ctx.lineTo(p.x, p.y);
ctx.strokeStyle = this.alphaColor(obj.color, 0.2);
ctx.lineWidth = 1;
ctx.stroke();
}
}
// Draw cross braces
for (let i = 0; i < segments; i++) {
const ci = obj.constraintStart + segments * 2 + i;
if (ci >= sim.constraints.length)
break;
const c = sim.constraints[ci];
if (c.torn)
continue;
const pa = sim.points[c.a];
const pb = sim.points[c.b];
ctx.beginPath();
ctx.moveTo(pa.x, pa.y);
ctx.lineTo(pb.x, pb.y);
ctx.strokeStyle = this.alphaColor(obj.color, 0.15);
ctx.lineWidth = 1;
ctx.stroke();
}
// Draw pins
for (let i = 0; i < obj.pointCount; i++) {
const p = sim.points[pStart + i];
if (p.pinned) {
ctx.beginPath();
ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.fill();
}
}
}
renderBall(sim, obj) {
const ctx = this.ctx;
const p = sim.points[obj.pointStart];
const r = p.radius;
// Glow
const gradient = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, r * 2);
gradient.addColorStop(0, this.alphaColor(obj.color, 0.4));
gradient.addColorStop(1, 'rgba(0,0,0,0)');
ctx.beginPath();
ctx.arc(p.x, p.y, r * 2, 0, Math.PI * 2);
ctx.fillStyle = gradient;
ctx.fill();
// Ball
ctx.beginPath();
ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
ctx.fillStyle = obj.color;
ctx.fill();
ctx.strokeStyle = this.alphaColor(obj.color, 0.8);
ctx.lineWidth = 2;
ctx.stroke();
}
renderPoints(sim) {
const ctx = this.ctx;
for (let i = 0; i < sim.points.length; i++) {
const p = sim.points[i];
const obj = sim.objects.find(o => o.pointStart <= i && i < o.pointStart + o.pointCount);
const color = obj ? obj.color : '#fff';
ctx.beginPath();
ctx.arc(p.x, p.y, p.pinned ? 4 : config_1.CONFIG.POINT_RADIUS, 0, Math.PI * 2);
if (p.pinned) {
ctx.fillStyle = '#fff';
}
else {
ctx.fillStyle = color;
}
ctx.fill();
}
}
alphaColor(hex, alpha) {
// Parse hex color
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})`;
}
}
exports.Renderer = Renderer;
};
// ── 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 config_1 = require("./config");
class InputHandler {
constructor(canvas, sim) {
this.tool = null;
this.mouseDown = false;
this.rightMouseDown = false;
this.shiftDown = false;
this.mouseX = 0;
this.mouseY = 0;
this.mouseStartX = 0;
this.mouseStartY = 0;
this.lastMouseX = 0;
this.lastMouseY = 0;
// For rope drawing preview
this.ropePreviewEnd = false;
this.canvas = canvas;
this.sim = sim;
this.setupListeners();
}
setupListeners() {
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());
window.addEventListener('keydown', (e) => this.onKeyDown(e));
window.addEventListener('keyup', (e) => this.onKeyUp(e));
}
getCanvasPos(e) {
const rect = this.canvas.getBoundingClientRect();
const scaleX = this.sim.canvasW / rect.width;
const scaleY = this.sim.canvasH / rect.height;
return {
x: (e.clientX - rect.left) * scaleX,
y: (e.clientY - rect.top) * scaleY,
};
}
onMouseDown(e) {
const pos = this.getCanvasPos(e);
this.mouseX = pos.x;
this.mouseY = pos.y;
this.lastMouseX = pos.x;
this.lastMouseY = pos.y;
if (e.button === 2 || e.shiftKey) {
// Right click or shift: tear mode
this.rightMouseDown = true;
this.mouseStartX = pos.x;
this.mouseStartY = pos.y;
this.onTear(pos.x, pos.y);
return;
}
if (e.altKey || e.ctrlKey) {
// Alt/Ctrl: toggle pin
this.sim.togglePin(pos.x, pos.y);
return;
}
this.mouseDown = true;
this.mouseStartX = pos.x;
this.mouseStartY = pos.y;
if (this.tool !== null) {
// Starting to draw a new object
// Rope and cloth need drag to define size
// Soft body and ball are instant
if (this.tool === 'ball') {
this.spawnObject(pos.x, pos.y);
}
else if (this.tool === 'softBody') {
this.spawnObject(pos.x, pos.y);
}
// rope and cloth: wait for mouseup or drag
}
else {
// No tool selected: try to grab
this.sim.grabPointAt(pos.x, pos.y);
}
}
onMouseMove(e) {
const pos = this.getCanvasPos(e);
this.lastMouseX = this.mouseX;
this.lastMouseY = this.mouseY;
this.mouseX = pos.x;
this.mouseY = pos.y;
if (this.rightMouseDown) {
this.onTear(pos.x, pos.y);
return;
}
if (this.sim.isDragging) {
this.sim.updateGrab(pos.x, pos.y);
}
else if (this.tool === 'rope' && this.mouseDown) {
// Live rope preview handled in renderer via mouse coords
}
else if (this.tool === 'cloth' && this.mouseDown) {
// Live cloth preview
}
}
onMouseUp(e) {
const pos = this.getCanvasPos(e);
this.mouseX = pos.x;
this.mouseY = pos.y;
if (this.rightMouseDown) {
this.rightMouseDown = false;
return;
}
if (this.mouseDown && this.tool !== null) {
// Finish drawing
if (this.tool === 'rope') {
this.spawnRope(this.mouseStartX, this.mouseStartY, pos.x, pos.y);
}
else if (this.tool === 'cloth') {
this.spawnCloth(this.mouseStartX, this.mouseStartY, pos.x, pos.y);
}
}
if (this.sim.isDragging) {
this.sim.releasePoint();
}
this.mouseDown = false;
}
onTear(x, y) {
// Tear along line from last mouse position to current
this.sim.tearAlongLine(this.lastMouseX, this.lastMouseY, x, y);
}
spawnRope(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
const length = Math.sqrt(dx * dx + dy * dy);
const segments = config_1.CONFIG.ROPE_SEGMENTS;
const spacing = length / segments;
this.sim.spawnRope(x1, y1, segments, spacing, config_1.CONFIG.COLORS.rope);
}
spawnCloth(x1, y1, x2, y2) {
const cols = config_1.CONFIG.CLOTH_COLS;
const rows = config_1.CONFIG.CLOTH_ROWS;
const spacing = config_1.CONFIG.CLOTH_SPACING;
// Place cloth with top-left at mouse start
this.sim.spawnCloth(x1, y1, cols, rows, spacing, config_1.CONFIG.COLORS.cloth, 'corners');
}
spawnObject(x, y) {
if (this.tool === 'ball') {
this.sim.spawnBall(x, y, config_1.CONFIG.BALL_RADIUS, config_1.CONFIG.COLORS.ball);
}
else if (this.tool === 'softBody') {
this.sim.spawnSoftBody(x, y, config_1.CONFIG.SOFT_BODY_RADIUS, config_1.CONFIG.SOFT_BODY_SEGMENTS, config_1.CONFIG.COLORS.softBody);
}
}
onKeyDown(e) {
if (e.key === 'Shift')
this.shiftDown = true;
if (e.key === ' ') {
e.preventDefault();
this.sim.paused = !this.sim.paused;
if (this.onTogglePause)
this.onTogglePause(this.sim.paused);
}
if (e.key === 'ArrowRight' && this.sim.paused) {
e.preventDefault();
if (this.onStep)
this.onStep();
}
// Tool shortcuts
if (e.key === 'r' || e.key === 'R')
this.selectTool('rope');
if (e.key === 'c' || e.key === 'C')
this.selectTool('cloth');
if (e.key === 's' || e.key === 'S')
this.selectTool('softBody');
if (e.key === 'b' || e.key === 'B')
this.selectTool('ball');
if (e.key === 'Escape')
this.selectTool(null);
if (e.key === '1')
this.selectTool('rope');
if (e.key === '2')
this.selectTool('cloth');
if (e.key === '3')
this.selectTool('softBody');
if (e.key === '4')
this.selectTool('ball');
}
onKeyUp(e) {
if (e.key === 'Shift')
this.shiftDown = false;
}
selectTool(tool) {
this.tool = tool;
if (this.onToolSelect)
this.onToolSelect(tool);
}
}
exports.InputHandler = InputHandler;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.6-27B-MTP-GGUF-IQ4_NL","provider":"brigid","persona":"typescript","sessionId":"004ccc55-b92f-4ff3-91fd-dec9fbd7aef7","tokensIn":288650,"tokensOut":7120,"tokensTotal":295770,"cost":0,"turns":12,"toolCalls":11,"failedToolCalls":0,"timestamp":"2026-08-04T01:57:10.965Z"} -->