// phys.js — модуль физических хелперов для учебника Физика 10
// Экспорт в window.PHYS = { ... }
(function(){
'use strict';

// === Стрелка вектора (2D) ===
function drawArrow(x1, y1, x2, y2, color, width, headSize) {
  width = width || 2;
  headSize = headSize || 10;
  const dx = x2 - x1, dy = y2 - y1;
  const len = Math.sqrt(dx*dx + dy*dy);
  if (len < 1e-6) return '';
  const ux = dx / len, uy = dy / len;
  const px = -uy, py = ux;
  const h = headSize, w = headSize * 0.6;
  const bx = x2 - ux*h, by = y2 - uy*h;
  const lx = bx + px*w, ly = by + py*w;
  const rx = bx - px*w, ry = by - py*w;
  return `<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${bx.toFixed(1)}" y2="${by.toFixed(1)}" stroke="${color}" stroke-width="${width}" stroke-linecap="round"/>`
       + `<polygon points="${x2.toFixed(1)},${y2.toFixed(1)} ${lx.toFixed(1)},${ly.toFixed(1)} ${rx.toFixed(1)},${ry.toFixed(1)}" fill="${color}"/>`;
}

// === Линии электрического поля от точечного заряда ===
function fieldLinesPointCharge(cx, cy, sign, scale, numLines) {
  numLines = numLines || 16;
  scale = scale || 80;
  let s = '';
  const color = sign > 0 ? '#dc2626' : '#2563eb';
  for (let i = 0; i < numLines; i++) {
    const a = 2 * Math.PI * i / numLines;
    const r1 = 18, r2 = scale;
    const x1 = cx + r1*Math.cos(a), y1 = cy + r1*Math.sin(a);
    const x2 = cx + r2*Math.cos(a), y2 = cy + r2*Math.sin(a);
    if (sign > 0) s += drawArrow(x1, y1, x2, y2, color, 1.4, 7);
    else          s += drawArrow(x2, y2, x1, y1, color, 1.4, 7);
  }
  return s;
}

// === Обозначение заряда (кружок с +/-) ===
function chargeMark(cx, cy, sign, r, label) {
  r = r || 14;
  const color = sign > 0 ? '#dc2626' : '#2563eb';
  const fill = sign > 0 ? '#fecaca' : '#bfdbfe';
  let s = '';
  s += `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${fill}" stroke="${color}" stroke-width="2"/>`;
  if (sign > 0) {
    s += `<line x1="${cx-r*0.5}" y1="${cy}" x2="${cx+r*0.5}" y2="${cy}" stroke="${color}" stroke-width="2.5"/>`;
    s += `<line x1="${cx}" y1="${cy-r*0.5}" x2="${cx}" y2="${cy+r*0.5}" stroke="${color}" stroke-width="2.5"/>`;
  } else {
    s += `<line x1="${cx-r*0.5}" y1="${cy}" x2="${cx+r*0.5}" y2="${cy}" stroke="${color}" stroke-width="2.5"/>`;
  }
  if (label) {
    s += `<text x="${cx+r+4}" y="${cy+4}" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="${color}">${label}</text>`;
  }
  return s;
}

// === Магнитное поле сквозь экран (сетка крестиков или точек) ===
function magneticFieldGrid(x0, y0, w, h, cols, rows, direction) {
  // direction: 'in' = крест (× — вошло в плоскость), 'out' = точка (• — вышло)
  let s = '';
  const dx = w / (cols - 1), dy = h / (rows - 1);
  const color = '#7c3aed';
  for (let i = 0; i < cols; i++) {
    for (let j = 0; j < rows; j++) {
      const cx = x0 + i * dx, cy = y0 + j * dy;
      s += `<circle cx="${cx}" cy="${cy}" r="6" fill="white" stroke="${color}" stroke-width="1.3"/>`;
      if (direction === 'in') {
        s += `<line x1="${cx-4}" y1="${cy-4}" x2="${cx+4}" y2="${cy+4}" stroke="${color}" stroke-width="1.5"/>`;
        s += `<line x1="${cx-4}" y1="${cy+4}" x2="${cx+4}" y2="${cy-4}" stroke="${color}" stroke-width="1.5"/>`;
      } else {
        s += `<circle cx="${cx}" cy="${cy}" r="2.2" fill="${color}"/>`;
      }
    }
  }
  return s;
}

// === Молекула газа (частица) ===
function molecule(x, y, r, color) {
  r = r || 4;
  color = color || '#2563eb';
  return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="${r}" fill="${color}" stroke="#0f172a" stroke-width="0.6"/>`;
}

// === Симуляция газа (упругое столкновение со стенками) ===
function createGasSim(opts) {
  opts = opts || {};
  const N = opts.N || 30;
  const W = opts.W || 320;
  const H = opts.H || 220;
  const baseSpeed = opts.speed || 60;  // px/s
  const r = opts.r || 4;
  const particles = [];
  for (let i = 0; i < N; i++) {
    particles.push({
      x: r + Math.random() * (W - 2*r),
      y: r + Math.random() * (H - 2*r),
      vx: (Math.random() - 0.5) * 2 * baseSpeed,
      vy: (Math.random() - 0.5) * 2 * baseSpeed
    });
  }
  return {
    W: W, H: H, r: r, particles: particles,
    step(dt) {
      for (const p of particles) {
        p.x += p.vx * dt; p.y += p.vy * dt;
        if (p.x < r) { p.x = r; p.vx = -p.vx; }
        if (p.x > W - r) { p.x = W - r; p.vx = -p.vx; }
        if (p.y < r) { p.y = r; p.vy = -p.vy; }
        if (p.y > H - r) { p.y = H - r; p.vy = -p.vy; }
      }
    },
    render(color) {
      color = color || '#2563eb';
      return particles.map(p => molecule(p.x, p.y, r, color)).join('');
    },
    setSpeed(scale) {
      for (const p of particles) { p.vx *= scale; p.vy *= scale; }
    }
  };
}

// === Электрические схемы: компоненты ===
// orientation: 'h' (горизонтально, по умолчанию) или 'v' (вертикально)
function batteryEMF(x, y, EMF, orientation) {
  orientation = orientation || 'h';
  let s = '';
  if (orientation === 'h') {
    s += `<line x1="${x-3}" y1="${y-18}" x2="${x-3}" y2="${y+18}" stroke="#0f172a" stroke-width="2.5"/>`;
    s += `<line x1="${x+3}" y1="${y-9}" x2="${x+3}" y2="${y+9}" stroke="#0f172a" stroke-width="2.5"/>`;
    s += `<text x="${x-3}" y="${y-24}" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#0f172a">+</text>`;
    s += `<text x="${x+3}" y="${y-24}" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#0f172a">&#8722;</text>`;
    if (EMF !== undefined) s += `<text x="${x}" y="${y+34}" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="12" fill="#0f172a">&#949; = ${EMF}</text>`;
  } else {
    s += `<line x1="${x-18}" y1="${y-3}" x2="${x+18}" y2="${y-3}" stroke="#0f172a" stroke-width="2.5"/>`;
    s += `<line x1="${x-9}" y1="${y+3}" x2="${x+9}" y2="${y+3}" stroke="#0f172a" stroke-width="2.5"/>`;
    if (EMF !== undefined) s += `<text x="${x+24}" y="${y+4}" font-family="JetBrains Mono,monospace" font-size="12" fill="#0f172a">&#949; = ${EMF}</text>`;
  }
  return s;
}

function resistor(x, y, R, orientation) {
  orientation = orientation || 'h';
  let s = '';
  if (orientation === 'h') {
    s += `<rect x="${x-20}" y="${y-7}" width="40" height="14" fill="white" stroke="#0f172a" stroke-width="1.6"/>`;
    if (R !== undefined) s += `<text x="${x}" y="${y+25}" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="12" fill="#0f172a">R = ${R}</text>`;
  } else {
    s += `<rect x="${x-7}" y="${y-20}" width="14" height="40" fill="white" stroke="#0f172a" stroke-width="1.6"/>`;
    if (R !== undefined) s += `<text x="${x+18}" y="${y+4}" font-family="JetBrains Mono,monospace" font-size="12" fill="#0f172a">R = ${R}</text>`;
  }
  return s;
}

function capacitorSymbol(x, y, C, orientation) {
  orientation = orientation || 'h';
  let s = '';
  if (orientation === 'h') {
    s += `<line x1="${x-3}" y1="${y-14}" x2="${x-3}" y2="${y+14}" stroke="#0f172a" stroke-width="2.5"/>`;
    s += `<line x1="${x+3}" y1="${y-14}" x2="${x+3}" y2="${y+14}" stroke="#0f172a" stroke-width="2.5"/>`;
    if (C !== undefined) s += `<text x="${x}" y="${y+30}" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="12" fill="#0f172a">C = ${C}</text>`;
  }
  return s;
}

function ammeterSymbol(x, y, r) {
  r = r || 14;
  return `<circle cx="${x}" cy="${y}" r="${r}" fill="white" stroke="#0f172a" stroke-width="1.6"/>`
       + `<text x="${x}" y="${y+5}" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#d97706">A</text>`;
}

function voltmeterSymbol(x, y, r) {
  r = r || 14;
  return `<circle cx="${x}" cy="${y}" r="${r}" fill="white" stroke="#0f172a" stroke-width="1.6"/>`
       + `<text x="${x}" y="${y+5}" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#2563eb">V</text>`;
}

function lightbulbSymbol(x, y, r) {
  r = r || 14;
  let s = '';
  s += `<circle cx="${x}" cy="${y}" r="${r}" fill="#fef3c7" stroke="#0f172a" stroke-width="1.6"/>`;
  s += `<line x1="${x-r*0.7}" y1="${y-r*0.7}" x2="${x+r*0.7}" y2="${y+r*0.7}" stroke="#0f172a" stroke-width="1.4"/>`;
  s += `<line x1="${x-r*0.7}" y1="${y+r*0.7}" x2="${x+r*0.7}" y2="${y-r*0.7}" stroke="#0f172a" stroke-width="1.4"/>`;
  return s;
}

function inductorSymbol(x, y, L, orientation) {
  orientation = orientation || 'h';
  let s = '';
  if (orientation === 'h') {
    for (let i = 0; i < 4; i++) {
      const cx = x - 15 + i * 10;
      s += `<path d="M ${cx-5} ${y} A 5 5 0 0 1 ${cx+5} ${y}" fill="none" stroke="#0f172a" stroke-width="1.6"/>`;
    }
    if (L !== undefined) s += `<text x="${x}" y="${y+22}" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="12" fill="#0f172a">L = ${L}</text>`;
  }
  return s;
}

function wire(x1, y1, x2, y2) {
  return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#0f172a" stroke-width="1.6"/>`;
}

// === Эталонные константы ===
const CONST = {
  k: 9e9,           // Кулона
  e: 1.6e-19,       // элементарный заряд
  eps0: 8.85e-12,   // электрическая постоянная
  kB: 1.38e-23,     // Больцмана
  NA: 6.022e23,     // Авогадро
  R: 8.314,         // универсальная газовая
  c: 3e8,           // скорость света
  g: 9.8,           // ускорение свободного падения
  atm: 101325,      // 1 атм в Па
  T0: 273.15        // ноль Цельсия в К
};

// === Конвертеры единиц ===
function celsiusToKelvin(t) { return t + 273.15; }
function kelvinToCelsius(T) { return T - 273.15; }
function atmToPa(p) { return p * 101325; }
function paToAtm(p) { return p / 101325; }
function litersToM3(V) { return V / 1000; }
function m3ToLiters(V) { return V * 1000; }

// === Расширения для Физики 8 (тепловые явления) ===

// === Цвет по температуре: 240° (синий) → 0° (красный) ===
function tempColor(t, tMin, tMax) {
  const u = Math.max(0, Math.min(1, (t - tMin) / (tMax - tMin)));
  const hue = 240 * (1 - u);
  return `hsl(${hue.toFixed(0)},72%,52%)`;
}

// === Термометр (вертикальная шкала, столбик ртути по температуре) ===
function thermometer(x, y, h, tMin, tMax, tValue) {
  // x, y — верх корпуса; h — высота столбика
  const reservoirR = 11;
  const tubeW = 8;
  const u = Math.max(0, Math.min(1, (tValue - tMin) / (tMax - tMin)));
  const fillH = h * u;
  const color = tempColor(tValue, tMin, tMax);
  let s = '';
  // Корпус (стекло)
  s += `<rect x="${x - tubeW/2}" y="${y}" width="${tubeW}" height="${h}" rx="${tubeW/2}" fill="#fff" stroke="#0f172a" stroke-width="1.4"/>`;
  // Столбик ртути
  s += `<rect x="${x - tubeW/2 + 1.5}" y="${y + h - fillH}" width="${tubeW - 3}" height="${fillH}" rx="${(tubeW-3)/2}" fill="${color}"/>`;
  // Резервуар
  s += `<circle cx="${x}" cy="${y + h + reservoirR - 1}" r="${reservoirR}" fill="${color}" stroke="#0f172a" stroke-width="1.4"/>`;
  // Деления (5)
  for (let i = 0; i <= 5; i++) {
    const ty = y + h * i / 5;
    const tv = tMax - (tMax - tMin) * i / 5;
    s += `<line x1="${x + tubeW/2 + 1}" y1="${ty}" x2="${x + tubeW/2 + 6}" y2="${ty}" stroke="#0f172a" stroke-width="1"/>`;
    s += `<text x="${x + tubeW/2 + 9}" y="${ty + 3.5}" font-family="JetBrains Mono,monospace" font-size="10" fill="#0f172a">${tv.toFixed(0)}</text>`;
  }
  // Подпись текущего значения
  s += `<text x="${x - tubeW/2 - 4}" y="${y - 4}" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" font-weight="700" fill="${color}">${tValue.toFixed(0)} &#176;C</text>`;
  return s;
}

// === Калориметр-стакан с жидкостью ===
function calorimeter(x, y, w, h, fillFrac, liquidColor) {
  fillFrac = Math.max(0, Math.min(1, fillFrac || 0.6));
  liquidColor = liquidColor || '#60a5fa';
  const wall = 4;
  const innerH = h - wall;
  const liqH = innerH * fillFrac;
  let s = '';
  // Внешний контур
  s += `<path d="M ${x} ${y} L ${x} ${y+h} Q ${x} ${y+h+6} ${x+8} ${y+h+6} L ${x+w-8} ${y+h+6} Q ${x+w} ${y+h+6} ${x+w} ${y+h} L ${x+w} ${y} L ${x+w-wall} ${y} L ${x+w-wall} ${y+h-2} L ${x+wall} ${y+h-2} L ${x+wall} ${y} Z" fill="#e0f2fe" stroke="#0f172a" stroke-width="1.5"/>`;
  // Жидкость
  s += `<rect x="${x+wall+1}" y="${y + innerH - liqH + wall/2}" width="${w - 2*wall - 2}" height="${liqH}" fill="${liquidColor}" opacity="0.78"/>`;
  // Мениск
  s += `<line x1="${x+wall+1}" y1="${y + innerH - liqH + wall/2}" x2="${x+w-wall-1}" y2="${y + innerH - liqH + wall/2}" stroke="${liquidColor}" stroke-width="1.6" opacity="0.95"/>`;
  return s;
}

// === Симуляция теплопроводности по стержню (одномерное уравнение тепла) ===
// Возвращает объект с .step(dt), .render(x, y, w, h), .reset(), .setTHot(t), .setTCold(t)
function createHeatBar(opts) {
  opts = opts || {};
  const N = opts.N || 30;
  const tHot = opts.tHot != null ? opts.tHot : 200;
  const tCold = opts.tCold != null ? opts.tCold : 0;
  const alpha = opts.alpha || 0.25;  // коэф. диффузии (отн. ед.)
  const T = new Array(N);
  for (let i = 0; i < N; i++) T[i] = tCold;
  return {
    N: N, alpha: alpha, T: T,
    _tHot: tHot, _tCold: tCold,
    setTHot(v) { this._tHot = v; },
    setTCold(v) { this._tCold = v; },
    reset() {
      for (let i = 0; i < this.N; i++) this.T[i] = this._tCold;
    },
    step(dt) {
      // Конечно-разностное уравнение теплопроводности с граничными условиями Дирихле.
      const Tnew = new Array(this.N);
      Tnew[0] = this._tHot;
      Tnew[this.N - 1] = this._tCold;
      for (let i = 1; i < this.N - 1; i++) {
        Tnew[i] = this.T[i] + this.alpha * dt * (this.T[i-1] - 2*this.T[i] + this.T[i+1]);
      }
      this.T = Tnew;
    },
    render(x, y, w, h) {
      const segW = w / this.N;
      const tMin = Math.min(this._tCold, this._tHot);
      const tMax = Math.max(this._tCold, this._tHot);
      let s = '';
      for (let i = 0; i < this.N; i++) {
        const c = tempColor(this.T[i], tMin, tMax);
        s += `<rect x="${(x + i*segW).toFixed(1)}" y="${y}" width="${(segW + 0.5).toFixed(1)}" height="${h}" fill="${c}"/>`;
      }
      // Контур стержня
      s += `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="#0f172a" stroke-width="1.5"/>`;
      return s;
    }
  };
}

// === График фазовых переходов T(t) с горизонтальными плато ===
// points: массив сегментов [{tStart, tEnd, Tstart, Tend, label?}]
// W, H, pad — размеры графика; tMaxAll, TminAll, TmaxAll — диапазоны
function phaseGraphTT(W, H, pad, points, tMaxAll, TminAll, TmaxAll) {
  const toX = t => pad + (W - 2*pad) * t / tMaxAll;
  const toY = T => H - pad - (H - 2*pad) * (T - TminAll) / (TmaxAll - TminAll);
  let s = '';
  // Оси
  s += `<line x1="${pad}" y1="${H-pad}" x2="${W-pad}" y2="${H-pad}" stroke="#0f172a" stroke-width="1.5"/>`;
  s += `<line x1="${pad}" y1="${pad}" x2="${pad}" y2="${H-pad}" stroke="#0f172a" stroke-width="1.5"/>`;
  s += `<text x="${W-pad+4}" y="${H-pad+4}" font-family="Inter,sans-serif" font-size="11" fill="#0f172a">t</text>`;
  s += `<text x="${pad-4}" y="${pad-4}" text-anchor="end" font-family="Inter,sans-serif" font-size="11" fill="#0f172a">T,&#176;C</text>`;
  // Сегменты
  let d = '';
  let first = true;
  for (const seg of points) {
    if (first) { d += `M ${toX(seg.tStart).toFixed(1)} ${toY(seg.Tstart).toFixed(1)} `; first = false; }
    d += `L ${toX(seg.tEnd).toFixed(1)} ${toY(seg.Tend).toFixed(1)} `;
  }
  s += `<path d="${d}" stroke="#dc2626" stroke-width="2.5" fill="none" stroke-linejoin="round" stroke-linecap="round"/>`;
  // Подписи сегментов
  for (const seg of points) {
    if (seg.label) {
      const mx = (toX(seg.tStart) + toX(seg.tEnd)) / 2;
      const my = (toY(seg.Tstart) + toY(seg.Tend)) / 2 - 8;
      s += `<text x="${mx.toFixed(1)}" y="${my.toFixed(1)}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="600" fill="#475569">${seg.label}</text>`;
    }
  }
  return { svg: s, toX: toX, toY: toY };
}

// === Анимация конвекции — тороидальный поток частиц ===
// Используется в §4 Физики 8 (главный визуал «конвекция в жидкости/газе»).
// opts: { N — число частиц (default 24), w, h — размеры области, speed (отн. ед., default 1),
//        tHot, tCold — температуры для окраски (default 100, 20) }.
function createConvectionSim(opts) {
  opts = opts || {};
  const N = opts.N || 24;
  const w = opts.w || 220;
  const h = opts.h || 140;
  const speed = opts.speed != null ? opts.speed : 1;
  const tHot = opts.tHot != null ? opts.tHot : 100;
  const tCold = opts.tCold != null ? opts.tCold : 20;
  // Каждая частица движется по фазе вдоль контура прямоугольника:
  // правая сторона — вверх (нагрев / подъём), верхняя — влево, левая — вниз (охлаждение), нижняя — вправо.
  const parts = new Array(N);
  for (let i = 0; i < N; i++) parts[i] = { phase: i / N };
  return {
    N: N, w: w, h: h, speed: speed,
    _tHot: tHot, _tCold: tCold,
    setSpeed(v) { this.speed = v; },
    setHot(v) { this._tHot = v; },
    setCold(v) { this._tCold = v; },
    step(dt) {
      const dPhase = 0.18 * this.speed * dt;
      for (let i = 0; i < this.N; i++) {
        parts[i].phase = (parts[i].phase + dPhase) % 1;
        if (parts[i].phase < 0) parts[i].phase += 1;
      }
    },
    // Возвращает координаты частицы (cx, cy) и температуру по её положению.
    // phase ∈ [0,1): 0..0.25 — правая (подъём, t→tHot), 0.25..0.5 — верх (t=tHot),
    //                0.5..0.75 — левая (опускание, t→tCold), 0.75..1 — низ (t=tCold).
    _xy(phase, x, y) {
      const margin = 12;
      const W = this.w - 2 * margin;
      const H = this.h - 2 * margin;
      let lx, ly, t;
      if (phase < 0.25) {
        const k = phase / 0.25;
        lx = W; ly = H * (1 - k);
        t = this._tCold + (this._tHot - this._tCold) * k;
      } else if (phase < 0.5) {
        const k = (phase - 0.25) / 0.25;
        lx = W * (1 - k); ly = 0;
        t = this._tHot;
      } else if (phase < 0.75) {
        const k = (phase - 0.5) / 0.25;
        lx = 0; ly = H * k;
        t = this._tHot - (this._tHot - this._tCold) * k;
      } else {
        const k = (phase - 0.75) / 0.25;
        lx = W * k; ly = H;
        t = this._tCold;
      }
      return { cx: x + margin + lx, cy: y + margin + ly, t: t };
    },
    render(x, y) {
      const tMin = Math.min(this._tCold, this._tHot);
      const tMax = Math.max(this._tCold, this._tHot);
      let s = '';
      // Контур сосуда
      s += `<rect x="${x}" y="${y}" width="${this.w}" height="${this.h}" fill="#dbeafe" stroke="#0f172a" stroke-width="1.5" rx="6"/>`;
      // Нагреватель снизу (красная полоса)
      s += `<rect x="${x + 8}" y="${y + this.h - 6}" width="${this.w - 16}" height="6" fill="#dc2626" opacity="0.85" rx="3"/>`;
      // Частицы
      for (let i = 0; i < this.N; i++) {
        const p = this._xy(parts[i].phase, x, y);
        const c = tempColor(p.t, tMin, tMax);
        s += `<circle cx="${p.cx.toFixed(1)}" cy="${p.cy.toFixed(1)}" r="4" fill="${c}" stroke="#0f172a" stroke-width="0.6" opacity="0.9"/>`;
      }
      return s;
    }
  };
}

// === Электронные хелперы для электрических задач ===
// Параллельное и последовательное сопротивление
function Rseries() {
  let R = 0;
  for (let i = 0; i < arguments.length; i++) R += arguments[i];
  return R;
}
function Rparallel() {
  let inv = 0;
  for (let i = 0; i < arguments.length; i++) {
    if (arguments[i] > 0) inv += 1 / arguments[i];
  }
  return inv > 0 ? 1 / inv : Infinity;
}

// ======================================================================
// PHYSICS 7 HELPERS — силы, давление, гидростатика, барометры, энергия.
// Все размеры в SVG-пикселях; физические величины в СИ.
// ======================================================================

function forceVector(x, y, F, angleDeg, color, label, scale) {
  scale = scale || 1.5;
  const a = -angleDeg * Math.PI / 180;
  const x2 = x + F * scale * Math.cos(a);
  const y2 = y + F * scale * Math.sin(a);
  let s = drawArrow(x, y, x2, y2, color || '#0f172a', 2.5, 9);
  if (label) {
    const lx = x2 + 8 * Math.cos(a);
    const ly = y2 + 8 * Math.sin(a) + 4;
    s += `<text x="${lx.toFixed(1)}" y="${ly.toFixed(1)}" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="${color || '#0f172a'}">${label}</text>`;
  }
  return s;
}

function dynamometer(x, y, h, Fmax, Fcurr) {
  const w = 30;
  const springTop = y + 14, springBot = y + h - 28;
  const stretch = Math.max(0, Math.min(1, Fcurr / Fmax)) * (springBot - springTop);
  const pointerY = springTop + stretch;
  let s = `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="#fef3c7" stroke="#92400e" stroke-width="1.5" rx="4"/>`;
  const coils = 8;
  const coilH = (pointerY - springTop) / coils;
  let path = `M ${x + w/2} ${springTop}`;
  for (let i = 0; i < coils; i++) {
    path += ` L ${x + (i%2 ? w-6 : 6)} ${springTop + (i+0.5)*coilH}`;
  }
  path += ` L ${x + w/2} ${pointerY}`;
  s += `<path d="${path}" fill="none" stroke="#92400e" stroke-width="1.5"/>`;
  const ticks = 10;
  for (let i = 0; i <= ticks; i++) {
    const ty = springTop + (springBot - springTop) * i / ticks;
    s += `<line x1="${x + w}" y1="${ty}" x2="${x + w + 6}" y2="${ty}" stroke="#92400e" stroke-width="1.2"/>`;
    if (i % 2 === 0) {
      s += `<text x="${x + w + 10}" y="${ty + 4}" font-family="Inter,sans-serif" font-size="9" fill="#92400e">${(Fmax * i / ticks).toFixed(Fmax < 5 ? 1 : 0)}</text>`;
    }
  }
  s += `<line x1="${x + 5}" y1="${pointerY}" x2="${x + w - 5}" y2="${pointerY}" stroke="#dc2626" stroke-width="2.5"/>`;
  s += `<circle cx="${x + w/2}" cy="${pointerY + 8}" r="4" fill="#374151"/>`;
  s += `<text x="${x + w/2}" y="${y + h + 18}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#374151">F = ${Fcurr.toFixed(1)} Н</text>`;
  return s;
}

function blockOnSurface(x, y, w, h, label, weights) {
  let s = `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="#a78bfa" stroke="#5b21b6" stroke-width="1.5" rx="3"/>`;
  if (label) s += `<text x="${x + w/2}" y="${y + h/2 + 4}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#fff">${label}</text>`;
  if (weights && weights > 0) {
    const wH = 10, wW = w * 0.7;
    for (let i = 0; i < weights; i++) {
      const wy = y - (i + 1) * (wH + 2);
      s += `<rect x="${x + (w - wW)/2}" y="${wy}" width="${wW}" height="${wH}" fill="#475569" stroke="#1e293b" stroke-width="1" rx="2"/>`;
    }
  }
  return s;
}

function connectedVessels(x, y, kindA, kindB, levelY, fluidColor) {
  fluidColor = fluidColor || '#60a5fa';
  const widths = { cylinder: 60, wide: 100, narrow: 35 };
  const wA = widths[kindA] || 60, wB = widths[kindB] || 60;
  const h = 140, gap = 50;
  const xA = x, xB = x + wA + gap;
  let s = '';
  s += `<line x1="${xA}" y1="${y}" x2="${xA}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
  s += `<line x1="${xA + wA}" y1="${y}" x2="${xA + wA}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
  s += `<line x1="${xB}" y1="${y}" x2="${xB}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
  s += `<line x1="${xB + wB}" y1="${y}" x2="${xB + wB}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
  s += `<line x1="${xA}" y1="${y + h}" x2="${xA + wA}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
  s += `<line x1="${xB}" y1="${y + h}" x2="${xB + wB}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
  s += `<line x1="${xA + wA}" y1="${y + h - 6}" x2="${xB}" y2="${y + h - 6}" stroke="#0f172a" stroke-width="2"/>`;
  s += `<line x1="${xA + wA}" y1="${y + h}" x2="${xB}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
  const fillH = (y + h) - levelY;
  s += `<rect x="${xA + 1}" y="${levelY}" width="${wA - 2}" height="${fillH}" fill="${fluidColor}" opacity="0.75"/>`;
  s += `<rect x="${xB + 1}" y="${levelY}" width="${wB - 2}" height="${fillH}" fill="${fluidColor}" opacity="0.75"/>`;
  s += `<rect x="${xA + wA}" y="${y + h - 6}" width="${gap}" height="6" fill="${fluidColor}" opacity="0.75"/>`;
  s += `<line x1="${xA - 8}" y1="${levelY}" x2="${xB + wB + 8}" y2="${levelY}" stroke="#dc2626" stroke-width="1" stroke-dasharray="4 3"/>`;
  return s;
}

function hydraulicPress(x, y, sSmall, sLarge, fSmall, fluidColor) {
  fluidColor = fluidColor || '#60a5fa';
  const fLarge = fSmall * sLarge / sSmall;
  const scale = 6;
  const wSmall = Math.max(20, Math.sqrt(sSmall) * scale);
  const wLarge = Math.max(40, Math.sqrt(sLarge) * scale);
  const cylH = 60, baseY = y + 110, gap = 40;
  const xS = x, xL = x + wSmall + gap;
  let s = '';
  s += `<rect x="${xS}" y="${y + 50}" width="${wSmall}" height="${cylH}" fill="${fluidColor}" stroke="#0f172a" stroke-width="1.5" opacity="0.7"/>`;
  s += `<rect x="${xS - 3}" y="${y + 40}" width="${wSmall + 6}" height="14" fill="#374151" stroke="#0f172a" stroke-width="1.5" rx="2"/>`;
  s += `<rect x="${xL}" y="${y + 50}" width="${wLarge}" height="${cylH}" fill="${fluidColor}" stroke="#0f172a" stroke-width="1.5" opacity="0.7"/>`;
  s += `<rect x="${xL - 3}" y="${y + 40}" width="${wLarge + 6}" height="14" fill="#374151" stroke="#0f172a" stroke-width="1.5" rx="2"/>`;
  s += `<rect x="${xS}" y="${baseY}" width="${(xL + wLarge) - xS}" height="10" fill="${fluidColor}" stroke="#0f172a" stroke-width="1.5" opacity="0.7"/>`;
  s += drawArrow(xS + wSmall/2, y + 20, xS + wSmall/2, y + 40, '#dc2626', 2.5, 8);
  s += drawArrow(xL + wLarge/2, y + 20, xL + wLarge/2, y + 40, '#10b981', 2.5, 8);
  s += `<text x="${xS + wSmall/2}" y="${y + 12}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#dc2626">F_1 = ${fSmall.toFixed(0)} Н</text>`;
  s += `<text x="${xL + wLarge/2}" y="${y + 12}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#10b981">F_2 = ${fLarge.toFixed(0)} Н</text>`;
  s += `<text x="${xS + wSmall/2}" y="${y + 80}" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="#0f172a">S_1 = ${sSmall} см²</text>`;
  s += `<text x="${xL + wLarge/2}" y="${y + 80}" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="#0f172a">S_2 = ${sLarge} см²</text>`;
  return s;
}

function mercuryBarometer(x, y, hMm) {
  hMm = hMm == null ? 760 : hMm;
  const maxMm = 800, scale = 0.4;
  const tubeH = maxMm * scale, tubeW = 14;
  const cupY = y + tubeH + 10, cupW = 70, cupH = 26;
  const colH = hMm * scale, colTop = y + tubeH - colH;
  let s = '';
  s += `<rect x="${x - cupW/2 + tubeW/2}" y="${cupY}" width="${cupW}" height="${cupH}" fill="#94a3b8" stroke="#0f172a" stroke-width="1.5" rx="3"/>`;
  s += `<rect x="${x - cupW/2 + tubeW/2 + 3}" y="${cupY + 5}" width="${cupW - 6}" height="${cupH - 8}" fill="#475569" opacity="0.85"/>`;
  s += `<rect x="${x}" y="${y}" width="${tubeW}" height="${tubeH + 12}" fill="#e0f2fe" stroke="#0f172a" stroke-width="1.5"/>`;
  s += `<rect x="${x + 1}" y="${colTop}" width="${tubeW - 2}" height="${colH + 12}" fill="#475569"/>`;
  s += `<text x="${x + tubeW + 8}" y="${y + 14}" font-family="Inter,sans-serif" font-size="10" font-style="italic" fill="#64748b">вакуум</text>`;
  for (let mm = 0; mm <= maxMm; mm += 50) {
    const ty = y + tubeH - mm * scale;
    s += `<line x1="${x + tubeW}" y1="${ty}" x2="${x + tubeW + 6}" y2="${ty}" stroke="#0f172a" stroke-width="1"/>`;
    if (mm % 100 === 0) s += `<text x="${x + tubeW + 10}" y="${ty + 3}" font-family="Inter,sans-serif" font-size="9" fill="#0f172a">${mm}</text>`;
  }
  s += `<line x1="${x - 12}" y1="${colTop}" x2="${x}" y2="${colTop}" stroke="#dc2626" stroke-width="2"/>`;
  s += `<text x="${x - 14}" y="${colTop + 4}" text-anchor="end" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#dc2626">${hMm} мм</text>`;
  return s;
}

function aneroidBarometer(cx, cy, r, pressurePa) {
  const pMm = pressurePa / 133.322;
  const pMin = 720, pMax = 800;
  const angDeg = -210 + 240 * Math.max(0, Math.min(1, (pMm - pMin) / (pMax - pMin)));
  const angRad = angDeg * Math.PI / 180;
  let s = '';
  s += `<circle cx="${cx}" cy="${cy}" r="${r}" fill="#fff" stroke="#0f172a" stroke-width="2"/>`;
  s += `<circle cx="${cx}" cy="${cy}" r="${r - 8}" fill="none" stroke="#cbd5e1" stroke-width="1"/>`;
  for (let p = pMin; p <= pMax; p += 10) {
    const a = (-210 + 240 * (p - pMin) / (pMax - pMin)) * Math.PI / 180;
    const r1 = r - 4, r2 = r - 12;
    const x1 = cx + r1 * Math.cos(a), y1 = cy + r1 * Math.sin(a);
    const x2 = cx + r2 * Math.cos(a), y2 = cy + r2 * Math.sin(a);
    s += `<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="#0f172a" stroke-width="${p % 20 === 0 ? 2 : 1}"/>`;
    if (p % 20 === 0) {
      const rT = r - 22;
      const xT = cx + rT * Math.cos(a), yT = cy + rT * Math.sin(a) + 3;
      s += `<text x="${xT.toFixed(1)}" y="${yT.toFixed(1)}" text-anchor="middle" font-family="Inter,sans-serif" font-size="9" fill="#0f172a">${p}</text>`;
    }
  }
  const tipX = cx + (r - 18) * Math.cos(angRad), tipY = cy + (r - 18) * Math.sin(angRad);
  s += `<line x1="${cx}" y1="${cy}" x2="${tipX.toFixed(1)}" y2="${tipY.toFixed(1)}" stroke="#dc2626" stroke-width="3" stroke-linecap="round"/>`;
  s += `<circle cx="${cx}" cy="${cy}" r="4" fill="#374151"/>`;
  s += `<text x="${cx}" y="${cy + r + 16}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#374151">${pMm.toFixed(0)} мм рт. ст.</text>`;
  return s;
}

function uManometer(x, y, w, h, deltaH, fluidColor) {
  fluidColor = fluidColor || '#0891b2';
  const tube = 14;
  const xL = x, xR = x + w - tube;
  const baseY = y + h;
  let s = `<path d="M ${xL} ${y} L ${xL} ${baseY - tube/2} Q ${xL} ${baseY} ${xL + tube/2} ${baseY} L ${xR + tube/2} ${baseY} Q ${xR + tube} ${baseY} ${xR + tube} ${baseY - tube/2} L ${xR + tube} ${y}" fill="none" stroke="#0f172a" stroke-width="1.5"/>`;
  s += `<path d="M ${xL + tube} ${y} L ${xL + tube} ${baseY - tube/2 - 8}" fill="none" stroke="#0f172a" stroke-width="1.5" opacity="0.4"/>`;
  s += `<path d="M ${xR} ${y} L ${xR} ${baseY - tube/2 - 8}" fill="none" stroke="#0f172a" stroke-width="1.5" opacity="0.4"/>`;
  const lLevel = y + h * 0.45 + deltaH / 2;
  const rLevel = y + h * 0.45 - deltaH / 2;
  s += `<rect x="${xL + 1}" y="${lLevel}" width="${tube - 2}" height="${baseY - lLevel}" fill="${fluidColor}" opacity="0.85"/>`;
  s += `<rect x="${xR + 1}" y="${rLevel}" width="${tube - 2}" height="${baseY - rLevel}" fill="${fluidColor}" opacity="0.85"/>`;
  if (Math.abs(deltaH) > 1) {
    s += `<line x1="${xR + tube + 6}" y1="${lLevel}" x2="${xR + tube + 6}" y2="${rLevel}" stroke="#dc2626" stroke-width="1.5"/>`;
    s += `<text x="${xR + tube + 10}" y="${(lLevel + rLevel)/2 + 4}" font-family="Inter,sans-serif" font-size="10" font-weight="700" fill="#dc2626">Δh</text>`;
  }
  return s;
}

function rulerWithError(x, y, lenCm, mmPerDiv) {
  mmPerDiv = mmPerDiv || 1;
  const pxPerCm = 30;
  const w = lenCm * pxPerCm, h = 28;
  let s = `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="#fef3c7" stroke="#92400e" stroke-width="1.5" rx="2"/>`;
  const totalDivs = (lenCm * 10) / mmPerDiv;
  for (let i = 0; i <= totalDivs; i++) {
    const tx = x + (i * mmPerDiv / 10) * pxPerCm;
    const mm = i * mmPerDiv;
    const isCm = mm % 10 === 0, isHalfCm = mm % 5 === 0;
    const tickH = isCm ? 12 : (isHalfCm ? 8 : 5);
    s += `<line x1="${tx}" y1="${y}" x2="${tx}" y2="${y + tickH}" stroke="#0f172a" stroke-width="${isCm ? 1.5 : 1}"/>`;
    if (isCm) s += `<text x="${tx}" y="${y + h - 4}" text-anchor="middle" font-family="Inter,sans-serif" font-size="9" font-weight="600" fill="#0f172a">${mm/10}</text>`;
  }
  s += `<text x="${x + w + 6}" y="${y + h/2 + 4}" font-family="Inter,sans-serif" font-size="10" font-weight="700" fill="#92400e">см</text>`;
  return s;
}

function bimetal(x, y, w, h, deltaT) {
  const ang = Math.max(-30, Math.min(30, deltaT * 0.3));
  let s = `<g transform="translate(${x},${y}) rotate(${ang} 0 ${h/2})">`;
  s += `<rect x="0" y="0" width="${w}" height="${h/2}" fill="#fbbf24" stroke="#92400e" stroke-width="1"/>`;
  s += `<rect x="0" y="${h/2}" width="${w}" height="${h/2}" fill="#94a3b8" stroke="#374151" stroke-width="1"/>`;
  s += `<text x="${w/2}" y="${h + 14}" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="#374151">ΔT = ${deltaT.toFixed(0)} °C</text>`;
  s += `</g>`;
  return s;
}

function expandingRod(x, y, l0, alpha, deltaT) {
  const dl = l0 * alpha * deltaT * 1000;
  const len = l0 + dl;
  const h = 16;
  const color = tempColor(deltaT, 0, 100);
  let s = `<rect x="${x}" y="${y}" width="${len.toFixed(1)}" height="${h}" fill="${color}" stroke="#0f172a" stroke-width="1.5" rx="2"/>`;
  s += `<line x1="${x + l0}" y1="${y - 4}" x2="${x + l0}" y2="${y + h + 4}" stroke="#0f172a" stroke-width="1" stroke-dasharray="3 2"/>`;
  if (dl > 1) {
    s += drawArrow(x + l0, y + h + 14, x + len, y + h + 14, '#dc2626', 1.8, 6);
    s += `<text x="${x + l0 + dl/2}" y="${y + h + 28}" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" font-weight="700" fill="#dc2626">Δl = ${dl.toFixed(1)} px</text>`;
  }
  return s;
}

// === HillSlideSim — тележка скатывается с горки (для §42, закон сохранения) ===
class HillSlideSim {
  constructor(opts) {
    opts = opts || {};
    this.x0 = opts.x0 || 30;
    this.y0 = opts.y0 || 200;
    this.hStart = opts.hStart != null ? opts.hStart : 5;
    this.m = opts.mass || 1;
    this.g = opts.g || 9.8;
    this.friction = opts.friction || 0;
    this.scale = opts.scale || 30;
    this.reset();
  }
  reset() { this.t = 0; this.x = 0; this.v = 0; this.h = this.hStart; }
  step(dt) {
    const gEff = this.g * (1 - this.friction);
    this.t += dt;
    const dropped = this.hStart - this.h;
    if (this.h <= 0) { this.h = 0; this.v = Math.sqrt(2 * gEff * this.hStart); return; }
    this.v = Math.sqrt(2 * gEff * Math.max(0, dropped));
    this.x += this.v * dt;
    const L = this.hStart * 4;
    const xRel = Math.min(this.x, L) / L;
    this.h = this.hStart * Math.pow(1 - xRel, 2);
  }
  getEnergies() {
    return {
      Ek: 0.5 * this.m * this.v * this.v,
      Ep: this.m * this.g * this.h,
      Etot: 0.5 * this.m * this.v * this.v + this.m * this.g * this.h
    };
  }
  renderProfile() {
    const L = this.hStart * 4;
    const pxL = L * this.scale, pxH = this.hStart * this.scale;
    const baseY = this.y0;
    let path = `M ${this.x0} ${baseY - pxH}`;
    const N = 40;
    for (let i = 1; i <= N; i++) {
      const xRel = i / N;
      const yRel = Math.pow(1 - xRel, 2);
      path += ` L ${(this.x0 + pxL * xRel).toFixed(1)} ${(baseY - pxH * yRel).toFixed(1)}`;
    }
    let s = '';
    s += `<line x1="${this.x0 - 10}" y1="${baseY}" x2="${this.x0 + pxL + 30}" y2="${baseY}" stroke="#0f172a" stroke-width="2"/>`;
    s += `<path d="${path} L ${this.x0 + pxL} ${baseY} L ${this.x0} ${baseY} Z" fill="#86efac" opacity="0.6" stroke="#10b981" stroke-width="1.5"/>`;
    const cartX = this.x0 + this.x * this.scale;
    const cartY = baseY - this.h * this.scale;
    s += `<rect x="${(cartX - 10).toFixed(1)}" y="${(cartY - 14).toFixed(1)}" width="20" height="12" fill="#dc2626" stroke="#0f172a" stroke-width="1.5" rx="2"/>`;
    s += `<circle cx="${(cartX - 5).toFixed(1)}" cy="${(cartY - 2).toFixed(1)}" r="3" fill="#0f172a"/>`;
    s += `<circle cx="${(cartX + 5).toFixed(1)}" cy="${(cartY - 2).toFixed(1)}" r="3" fill="#0f172a"/>`;
    return s;
  }
}

// === PendulumSim — математический маятник (для §42) ===
class PendulumSim {
  constructor(opts) {
    opts = opts || {};
    this.cx = opts.cx || 150;
    this.cy = opts.cy || 40;
    this.L = opts.length || 1.2;
    this.m = opts.mass || 0.5;
    this.g = opts.g || 9.8;
    this.phi0 = (opts.angleDeg || 25) * Math.PI / 180;
    this.scale = opts.scale || 80;
    this.reset();
  }
  reset() { this.t = 0; this.phi = this.phi0; this.omega = 0; }
  step(dt) {
    const alpha = -(this.g / this.L) * Math.sin(this.phi);
    this.omega += alpha * dt;
    this.phi += this.omega * dt;
    this.t += dt;
  }
  getEnergies() {
    const h = this.L * (1 - Math.cos(this.phi));
    const v = Math.abs(this.omega) * this.L;
    return {
      Ek: 0.5 * this.m * v * v,
      Ep: this.m * this.g * h,
      Etot: 0.5 * this.m * v * v + this.m * this.g * h
    };
  }
  render() {
    const pxL = this.L * this.scale;
    const bx = this.cx + pxL * Math.sin(this.phi);
    const by = this.cy + pxL * Math.cos(this.phi);
    let s = '';
    s += `<rect x="${this.cx - 30}" y="${this.cy - 8}" width="60" height="8" fill="#475569" stroke="#0f172a" stroke-width="1"/>`;
    s += `<line x1="${this.cx}" y1="${this.cy}" x2="${bx.toFixed(1)}" y2="${by.toFixed(1)}" stroke="#0f172a" stroke-width="1.5"/>`;
    s += `<circle cx="${bx.toFixed(1)}" cy="${by.toFixed(1)}" r="9" fill="#dc2626" stroke="#7f1d1d" stroke-width="1.5"/>`;
    s += `<circle cx="${this.cx}" cy="${this.cy}" r="3" fill="#0f172a"/>`;
    return s;
  }
}

// === Экспорт ===
window.PHYS = {
  tempColor: tempColor,
  thermometer: thermometer,
  calorimeter: calorimeter,
  createHeatBar: createHeatBar,
  createConvectionSim: createConvectionSim,
  phaseGraphTT: phaseGraphTT,
  Rseries: Rseries,
  Rparallel: Rparallel,
  drawArrow: drawArrow,
  fieldLinesPointCharge: fieldLinesPointCharge,
  chargeMark: chargeMark,
  magneticFieldGrid: magneticFieldGrid,
  molecule: molecule,
  createGasSim: createGasSim,
  batteryEMF: batteryEMF,
  resistor: resistor,
  capacitorSymbol: capacitorSymbol,
  ammeterSymbol: ammeterSymbol,
  voltmeterSymbol: voltmeterSymbol,
  lightbulbSymbol: lightbulbSymbol,
  inductorSymbol: inductorSymbol,
  wire: wire,
  CONST: CONST,
  celsiusToKelvin: celsiusToKelvin,
  kelvinToCelsius: kelvinToCelsius,
  atmToPa: atmToPa,
  paToAtm: paToAtm,
  litersToM3: litersToM3,
  m3ToLiters: m3ToLiters,
  // Physics 7 — Phase 0
  forceVector: forceVector,
  dynamometer: dynamometer,
  blockOnSurface: blockOnSurface,
  connectedVessels: connectedVessels,
  hydraulicPress: hydraulicPress,
  mercuryBarometer: mercuryBarometer,
  aneroidBarometer: aneroidBarometer,
  uManometer: uManometer,
  rulerWithError: rulerWithError,
  bimetal: bimetal,
  expandingRod: expandingRod,
  HillSlideSim: HillSlideSim,
  PendulumSim: PendulumSim
};
})();
