方程图像优化2.0

优化了性能体验,减少了性能开销,加载速度明显减少,视觉反馈不变,同时优化了加载时的等待显示,不再是卡在原界面不动,而是会在一个新页面上方显示你要绘制的图像,然后下方显示正在计算...

适配设备:Pro

作者:hughx

下载量:28 · 点赞:0

可信源码风险提示:保存或下载前请阅读源码和权限声明。
SHA-25695076eefb7492d41acec409c72dd5738c59e9887efd1d9a0c27a7b1cde145d22
async function main() {
  // ==================== 数学函数映射 ====================
  const MATH_FUNCS = {
    abs: Math.abs, sqrt: Math.sqrt, cbrt: Math.cbrt,
    sin: Math.sin, cos: Math.cos, tan: Math.tan,
    asin: Math.asin, acos: Math.acos, atan: Math.atan, atan2: Math.atan2,
    sinh: Math.sinh, cosh: Math.cosh, tanh: Math.tanh,
    exp: Math.exp, log: Math.log, ln: Math.log,
    log2: Math.log2, log10: Math.log10,
    pow: Math.pow, min: Math.min, max: Math.max,
    round: Math.round, floor: Math.floor, ceil: Math.ceil,
    sign: Math.sign, trunc: Math.trunc, hypot: Math.hypot
  };
  const MATH_CONSTS = {
    pi: Math.PI, PI: Math.PI, e: Math.E, E: Math.E,
    LN2: Math.LN2, LN10: Math.LN10, SQRT2: Math.SQRT2
  };

  function stripMath(name) {
    return name.indexOf('Math.') === 0 ? name.slice(5) : name;
  }

  // ==================== 词法分析 ====================
  function tokenize(src) {
    const out = [];
    let i = 0;
    const isDigit = c => c >= '0' && c <= '9';
    const isAlpha = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_';
    while (i < src.length) {
      const c = src[i];
      if (c === ' ' || c === '\t' || c === '\n' || c === '\r') { i++; continue; }
      if (isDigit(c) || (c === '.' && isDigit(src[i + 1]))) {
        let j = i;
        while (j < src.length && (isDigit(src[j]) || src[j] === '.')) j++;
        if (j < src.length && (src[j] === 'e' || src[j] === 'E')) {
          let k = j + 1;
          if (k < src.length && (src[k] === '+' || src[k] === '-')) k++;
          if (k < src.length && isDigit(src[k])) {
            while (k < src.length && isDigit(src[k])) k++;
            j = k;
          }
        }
        out.push({ t: 'num', v: parseFloat(src.slice(i, j)) });
        i = j;
        continue;
      }
      if (isAlpha(c)) {
        let j = i;
        while (j < src.length && (isAlpha(src[j]) || isDigit(src[j]) || src[j] === '.')) j++;
        out.push({ t: 'id', v: src.slice(i, j) });
        i = j;
        continue;
      }
      if ('+-*/%^(),'.indexOf(c) >= 0) { out.push({ t: 'op', v: c }); i++; continue; }
      throw new Error('无法识别的字符 "' + c + '"');
    }
    return out;
  }

  // ==================== 语法分析 ====================
  function parseTokens(tokens) {
    let pos = 0;
    function isOp(v) { const t = tokens[pos]; return t && t.t === 'op' && t.v === v; }
    function eatOp(v) { if (isOp(v)) { pos++; return true; } return false; }

    function parseExpr() { return parseAdd(); }

    function parseAdd() {
      let node = parseMul();
      while (true) {
        const t = tokens[pos];
        if (t && t.t === 'op' && (t.v === '+' || t.v === '-')) {
          pos++;
          node = { k: 'bin', op: t.v, l: node, r: parseMul() };
        } else break;
      }
      return node;
    }

    function parseMul() {
      let node = parseUnary();
      while (true) {
        const t = tokens[pos];
        if (t && t.t === 'op' && (t.v === '*' || t.v === '/' || t.v === '%')) {
          pos++;
          node = { k: 'bin', op: t.v, l: node, r: parseUnary() };
        } else break;
      }
      return node;
    }

    function parseUnary() {
      const t = tokens[pos];
      if (t && t.t === 'op' && (t.v === '-' || t.v === '+')) {
        pos++;
        const a = parseUnary();
        return t.v === '-' ? { k: 'neg', a: a } : a;
      }
      return parsePow();
    }

    function parsePow() {
      const base = parseAtom();
      if (isOp('^')) {
        pos++;
        const exp = parseUnary();
        return { k: 'bin', op: '^', l: base, r: exp };
      }
      return base;
    }

    function parseAtom() {
      const t = tokens[pos];
      if (!t) throw new Error('表达式不完整');
      if (t.t === 'num') { pos++; return { k: 'num', v: t.v }; }
      if (t.t === 'id') {
        pos++;
        if (isOp('(')) {
          pos++;
          const args = [];
          if (!isOp(')')) {
            args.push(parseExpr());
            while (eatOp(',')) args.push(parseExpr());
          }
          if (!eatOp(')')) throw new Error('缺少右括号');
          return { k: 'call', name: t.v, args: args };
        }
        return { k: 'var', name: t.v };
      }
      if (t.t === 'op' && t.v === '(') {
        pos++;
        const e = parseExpr();
        if (!eatOp(')')) throw new Error('缺少右括号');
        return e;
      }
      throw new Error('意外的符号 "' + t.v + '"');
    }

    const ast = parseExpr();
    if (pos !== tokens.length) throw new Error('表达式存在多余内容');
    return ast;
  }

  // ==================== 编译为闭包树 ====================
  function compileNode(node) {
    switch (node.k) {
      case 'num': { const v = node.v; return function () { return v; }; }
      case 'var': {
        const n = node.name;
        if (n === 'x' || n === 'X') return function (x) { return x; };
        if (n === 'y' || n === 'Y') return function (x, y) { return y; };
        const key = stripMath(n);
        if (Object.prototype.hasOwnProperty.call(MATH_CONSTS, key)) {
          const v = MATH_CONSTS[key];
          return function () { return v; };
        }
        throw new Error('未知变量 ' + n);
      }
      case 'neg': { const a = compileNode(node.a); return function (x, y) { return -a(x, y); }; }
      case 'bin': {
        const l = compileNode(node.l);
        const r = compileNode(node.r);
        const op = node.op;
        if (op === '+') return function (x, y) { return l(x, y) + r(x, y); };
        if (op === '-') return function (x, y) { return l(x, y) - r(x, y); };
        if (op === '*') return function (x, y) { return l(x, y) * r(x, y); };
        if (op === '/') return function (x, y) { return l(x, y) / r(x, y); };
        if (op === '%') return function (x, y) { return l(x, y) % r(x, y); };
        if (op === '^') return function (x, y) { return Math.pow(l(x, y), r(x, y)); };
        throw new Error('未知运算符');
      }
      case 'call': {
        const key = stripMath(node.name);
        const f = MATH_FUNCS[key];
        if (!f) throw new Error('未知函数 ' + node.name);
        const args = node.args.map(compileNode);
        const n = args.length;
        if (n === 1) { const a0 = args[0]; return function (x, y) { return f(a0(x, y)); }; }
        if (n === 2) { const a0 = args[0], a1 = args[1]; return function (x, y) { return f(a0(x, y), a1(x, y)); }; }
        if (n === 3) { const a0 = args[0], a1 = args[1], a2 = args[2]; return function (x, y) { return f(a0(x, y), a1(x, y), a2(x, y)); }; }
        return function (x, y) {
          const vals = [];
          for (let i = 0; i < n; i++) vals.push(args[i](x, y));
          return f.apply(Math, vals);
        };
      }
    }
    throw new Error('未知表达式节点');
  }

  function hasVar(node, name) {
    if (!node) return false;
    if (node.k === 'var') { const n = node.name; return n === name || n === name.toUpperCase(); }
    if (node.k === 'neg') return hasVar(node.a, name);
    if (node.k === 'bin') return hasVar(node.l, name) || hasVar(node.r, name);
    if (node.k === 'call') {
      for (let i = 0; i < node.args.length; i++) {
        if (hasVar(node.args[i], name)) return true;
      }
    }
    return false;
  }

  function buildEvaluator(eqStr) {
    const idx = eqStr.indexOf('=');
    if (idx < 0) return null;
    try {
      const lhsAst = parseTokens(tokenize(eqStr.slice(0, idx)));
      const rhsAst = parseTokens(tokenize(eqStr.slice(idx + 1)));
      const lhsIsY = lhsAst.k === 'var' && (lhsAst.name === 'y' || lhsAst.name === 'Y');
      const rhsHasY = hasVar(rhsAst, 'y');
      if (lhsIsY && !rhsHasY) {
        const rhsFn = compileNode(rhsAst);
        const f = function (x) { return rhsFn(x, 0); };
        const probe = f(1);
        if (typeof probe !== 'number') return null;
        return { mode: 'explicit', f: f };
      }
      const lhsFn = compileNode(lhsAst);
      const rhsFn = compileNode(rhsAst);
      const F = function (x, y) { return lhsFn(x, y) - rhsFn(x, y); };
      const probe = F(1, 1);
      if (typeof probe !== 'number') return null;
      return { mode: 'implicit', F: F };
    } catch (e) {
      return null;
    }
  }

  function buildCurve(eqStr, color) {
    const ev = buildEvaluator(eqStr);
    if (!ev) return null;
    let F;
    if (ev.mode === 'explicit') {
      const f = ev.f;
      F = function (x, y) { return y - f(x); };
    } else {
      F = ev.F;
    }
    return { eq: eqStr, ev: ev, F: F, color: color };
  }

  // ==================== 工具函数 ====================
  function niceStep(range) {
    const target = range / 2;
    const cands = [1, 2, 5, 10, 15, 20, 30, 50];
    for (let i = 0; i < cands.length; i++) {
      if (target <= cands[i]) return cands[i];
    }
    return 50;
  }

  function fmtNum(v) {
    const r = Math.round(v * 100) / 100;
    if (Math.abs(r) < 1e-9) return '0';
    if (Number.isInteger(r)) return String(r);
    let s = r.toFixed(2);
    while (s.length > 1 && s.charAt(s.length - 1) === '0') s = s.slice(0, -1);
    return s;
  }

  // ==================== 隐式曲线采样 ====================
  // STEP=4,相邻点最大距离 = 4×√2 ≈ 5.66px,配合 6px 点大小完全无空隙
  function sampleImplicitCurve(F, GW, GH, OX, OY, scale, STEP) {
    const BISECT = 6;
    const cols = Math.floor(GW / STEP) + 2;
    const rows = Math.floor(GH / STEP) + 2;
    const vals = new Float64Array(cols * rows);
    const ok = new Uint8Array(cols * rows);
    const invScale = 1 / scale;

    for (let j = 0; j < rows; j++) {
      const yv = (OY - j * STEP) * invScale;
      const base = j * cols;
      for (let i = 0; i < cols; i++) {
        const xv = (i * STEP - OX) * invScale;
        let v;
        try { v = F(xv, yv); } catch (e) { v = NaN; }
        if (typeof v === 'number' && isFinite(v)) {
          vals[base + i] = v;
          ok[base + i] = 1;
        }
      }
    }

    const points = [];
    const seen = {};
    function pushPoint(px, py) {
      if (px < 0 || py < 0 || px > GW || py > GH) return;
      const ix = Math.round(px);
      const iy = Math.round(py);
      const key = ix + ':' + iy;
      if (seen[key]) return;
      seen[key] = 1;
      points.push({ x: ix, y: iy });
    }

    function bisect(x1, y1, x2, y2, v1pos) {
      let lo = 0, hi = 1;
      const dxe = x2 - x1, dye = y2 - y1;
      for (let k = 0; k < BISECT; k++) {
        const mid = (lo + hi) * 0.5;
        let vm;
        try { vm = F(x1 + dxe * mid, y1 + dye * mid); } catch (e) { break; }
        if (!isFinite(vm)) break;
        if ((vm > 0) === v1pos) lo = mid; else hi = mid;
      }
      const t = (lo + hi) * 0.5;
      return { x: x1 + dxe * t, y: y1 + dye * t };
    }

    for (let j = 0; j < rows - 1; j++) {
      const rowBase = j * cols;
      const y0 = (OY - j * STEP) * invScale;
      const y1 = (OY - (j + 1) * STEP) * invScale;
      for (let i = 0; i < cols - 1; i++) {
        const i00 = rowBase + i;
        const i10 = i00 + 1;
        const i01 = i00 + cols;
        const i11 = i01 + 1;
        if (!ok[i00] || !ok[i10] || !ok[i01] || !ok[i11]) continue;

        const v00 = vals[i00], v10 = vals[i10], v01 = vals[i01], v11 = vals[i11];
        const p00 = v00 > 0, p10 = v10 > 0, p01 = v01 > 0, p11 = v11 > 0;
        if (p00 === p10 && p10 === p01 && p01 === p11) continue;

        const x0 = (i * STEP - OX) * invScale;
        const x1 = ((i + 1) * STEP - OX) * invScale;

        if (p00 !== p10) { const p = bisect(x0, y0, x1, y0, p00); pushPoint(OX + p.x * scale, OY - p.y * scale); }
        if (p01 !== p11) { const p = bisect(x0, y1, x1, y1, p01); pushPoint(OX + p.x * scale, OY - p.y * scale); }
        if (p00 !== p01) { const p = bisect(x0, y0, x0, y1, p00); pushPoint(OX + p.x * scale, OY - p.y * scale); }
        if (p10 !== p11) { const p = bisect(x1, y0, x1, y1, p10); pushPoint(OX + p.x * scale, OY - p.y * scale); }
      }
    }
    return points;
  }

  // ==================== 显式曲线采样(屏幕距离自适应加密) ====================
  function sampleExplicitCurve(f, visX, GW, GH, OX, OY, scale, SAMPLES) {
    const points = [];
    const seen = {};
    const x0 = -visX;
    const dx = (visX * 2) / SAMPLES;

    function tryPoint(xv) {
      let yv;
      try { yv = f(xv); } catch (e) { return null; }
      if (typeof yv !== 'number' || !isFinite(yv)) return null;
      return { xv: xv, px: OX + xv * scale, py: OY - yv * scale };
    }

    function pushPixel(px, py) {
      if (px < 0 || py < 0 || px > GW || py > GH) return;
      const ix = Math.round(px);
      const iy = Math.round(py);
      const key = ix + ':' + iy;
      if (seen[key]) return;
      seen[key] = 1;
      points.push({ x: ix, y: iy });
    }

    let prev = null;
    for (let i = 0; i <= SAMPLES; i++) {
      const xv = x0 + i * dx;
      const p = tryPoint(xv);
      if (!p) { prev = null; continue; }

      if (p.px >= -3 && p.px <= GW + 3 && p.py >= -3 && p.py <= GH + 3) {
        pushPixel(p.px, p.py);
      }

      if (prev) {
        const ddx = p.px - prev.px;
        const ddy = p.py - prev.py;
        const dist = Math.sqrt(ddx * ddx + ddy * ddy);
        if (dist > 4) {
          const steps = Math.min(6, Math.ceil(dist / 3));
          for (let k = 1; k < steps; k++) {
            const t = k / steps;
            const mxv = prev.xv + (p.xv - prev.xv) * t;
            const mp = tryPoint(mxv);
            if (mp && mp.px >= -3 && mp.px <= GW + 3 && mp.py >= -3 && mp.py <= GH + 3) {
              pushPixel(mp.px, mp.py);
            }
          }
        }
      }
      prev = p;
    }
    return points;
  }

  // ==================== 牛顿迭代 ====================
  function newton2(F1, F2, x0, y0) {
    let x = x0, y = y0;
    const e = 1e-7;
    for (let k = 0; k < 12; k++) {
      let f1, f2;
      try { f1 = F1(x, y); f2 = F2(x, y); } catch (err) { return null; }
      if (!isFinite(f1) || !isFinite(f2)) return null;
      if (Math.abs(f1) < 1e-10 && Math.abs(f2) < 1e-10) break;

      let f1x, f1y, f2x, f2y;
      try {
        f1x = (F1(x + e, y) - f1) / e;
        f1y = (F1(x, y + e) - f1) / e;
        f2x = (F2(x + e, y) - f2) / e;
        f2y = (F2(x, y + e) - f2) / e;
      } catch (err) { return null; }
      if (!isFinite(f1x) || !isFinite(f1y) || !isFinite(f2x) || !isFinite(f2y)) return null;

      const det = f1x * f2y - f1y * f2x;
      if (Math.abs(det) < 1e-15) return null;

      const dx = (f1 * f2y - f1y * f2) / det;
      const dy = (f1x * f2 - f1 * f2x) / det;
      x -= dx; y -= dy;
      if (Math.abs(dx) < 1e-10 && Math.abs(dy) < 1e-10) break;
    }
    return { x: x, y: y };
  }

  // ==================== 双隐式交点(网格 + 牛顿) ====================
  function findIntersectionsGrid(F1, F2, OX, OY, scale, GW, GH) {
    const STEP = 8;
    const cols = Math.floor(GW / STEP) + 2;
    const rows = Math.floor(GH / STEP) + 2;
    const A = new Float64Array(cols * rows);
    const B = new Float64Array(cols * rows);
    const invScale = 1 / scale;

    for (let j = 0; j < rows; j++) {
      const yv = (OY - j * STEP) * invScale;
      const base = j * cols;
      for (let i = 0; i < cols; i++) {
        const xv = (i * STEP - OX) * invScale;
        let v1 = NaN, v2 = NaN;
        try { v1 = F1(xv, yv); } catch (e) {}
        try { v2 = F2(xv, yv); } catch (e) {}
        A[base + i] = v1;
        B[base + i] = v2;
      }
    }

    const results = [];
    const seen = {};
    function pushPoint(px, py) {
      const key = Math.round(px * 300) + ':' + Math.round(py * 300);
      if (seen[key]) return;
      seen[key] = 1;
      results.push({ x: px, y: py });
    }

    for (let j = 0; j < rows - 1; j++) {
      const rowBase = j * cols;
      for (let i = 0; i < cols - 1; i++) {
        const i00 = rowBase + i;
        const i10 = i00 + 1;
        const i01 = i00 + cols;
        const i11 = i01 + 1;
        const a00 = A[i00], a10 = A[i10], a01 = A[i01], a11 = A[i11];
        const b00 = B[i00], b10 = B[i10], b01 = B[i01], b11 = B[i11];
        if (!isFinite(a00) || !isFinite(a10) || !isFinite(a01) || !isFinite(a11)) continue;
        if (!isFinite(b00) || !isFinite(b10) || !isFinite(b01) || !isFinite(b11)) continue;

        const pa = a00 > 0;
        if ((a10 > 0) === pa && (a01 > 0) === pa && (a11 > 0) === pa) continue;
        const pb = b00 > 0;
        if ((b10 > 0) === pb && (b01 > 0) === pb && (b11 > 0) === pb) continue;

        const cx = (i * STEP + STEP * 0.5 - OX) * invScale;
        const cy = (OY - (j * STEP + STEP * 0.5)) * invScale;
        const sol = newton2(F1, F2, cx, cy);
        if (!sol || !isFinite(sol.x) || !isFinite(sol.y)) continue;

        let f1, f2;
        try { f1 = F1(sol.x, sol.y); f2 = F2(sol.x, sol.y); } catch (e) { continue; }
        if (!isFinite(f1) || !isFinite(f2)) continue;
        if (Math.abs(f1) > 1e-4 || Math.abs(f2) > 1e-4) continue;

        pushPoint(sol.x, sol.y);
      }
    }
    results.sort(function (p, q) { return p.x - q.x; });
    return results;
  }

  // ==================== 交点总入口 ====================
  function findIntersections(c1, c2, visX, visY, OX, OY, scale, GW, GH) {
    if (c1.ev.mode === 'explicit' && c2.ev.mode === 'explicit') {
      const f1 = c1.ev.f, f2 = c2.ev.f;
      const g = function (x) { return f1(x) - f2(x); };
      const xs = findRoots1D(g, -visX, visX, 80);
      const out = [];
      for (let i = 0; i < xs.length; i++) {
        const yv = f1(xs[i]);
        if (isFinite(yv) && Math.abs(yv) <= visY) out.push({ x: xs[i], y: yv });
      }
      return out;
    }
    if (c1.ev.mode === 'explicit') {
      const f = c1.ev.f, F = c2.F;
      const g = function (x) { return F(x, f(x)); };
      const xs = findRoots1D(g, -visX, visX, 80);
      const out = [];
      for (let i = 0; i < xs.length; i++) {
        const yv = f(xs[i]);
        if (isFinite(yv) && Math.abs(yv) <= visY) out.push({ x: xs[i], y: yv });
      }
      return out;
    }
    if (c2.ev.mode === 'explicit') {
      const f = c2.ev.f, F = c1.F;
      const g = function (x) { return F(x, f(x)); };
      const xs = findRoots1D(g, -visX, visX, 80);
      const out = [];
      for (let i = 0; i < xs.length; i++) {
        const yv = f(xs[i]);
        if (isFinite(yv) && Math.abs(yv) <= visY) out.push({ x: xs[i], y: yv });
      }
      return out;
    }
    return findIntersectionsGrid(c1.F, c2.F, OX, OY, scale, GW, GH);
  }

  // ==================== 一维求根 ====================
  function findRoots1D(g, lo, hi, N) {
    const step = (hi - lo) / N;
    const EPS = 1e-9;
    const roots = [];
    const minGap = step * 0.5;

    function safeEval(x) {
      try {
        const v = g(x);
        return (typeof v === 'number' && isFinite(v)) ? v : NaN;
      } catch (e) { return NaN; }
    }

    function pushRoot(x) {
      for (let i = roots.length - 1; i >= 0; i--) {
        if (Math.abs(roots[i] - x) < minGap) return;
      }
      roots.push(x);
    }

    let prevX = lo;
    let prevY = safeEval(lo);
    if (isFinite(prevY) && Math.abs(prevY) < EPS) pushRoot(lo);

    for (let i = 1; i <= N; i++) {
      const x = lo + i * step;
      const y = safeEval(x);

      if (isFinite(y)) {
        if (Math.abs(y) < EPS) {
          pushRoot(x);
        } else if (isFinite(prevY) && Math.abs(prevY) >= EPS &&
                   ((prevY > 0) !== (y > 0))) {
          let a = prevX, b = x, fa = prevY;
          for (let k = 0; k < 40; k++) {
            const m = (a + b) * 0.5;
            const fm = safeEval(m);
            if (!isFinite(fm)) break;
            if (fm === 0) { a = m; b = m; break; }
            if ((fa > 0) !== (fm > 0)) { b = m; } else { a = m; fa = fm; }
          }
          const root = (a + b) * 0.5;
          const gv = safeEval(root);
          if (isFinite(gv) && Math.abs(gv) < 1e-3) pushRoot(root);
        }
      }

      prevX = x;
      prevY = y;
    }
    roots.sort(function (a, b) { return a - b; });
    return roots;
  }

  // ==================== 输入流程 ====================
  const modeRes = await dialog.select({
    title: "绘制模式",
    message: "选择要绘制的图像数量",
    items: [
      { label: "单条图像", value: "single" },
      { label: "多条图像(最多3条)", value: "multi" }
    ],
    value: "single"
  });
  if (modeRes.action !== "confirm") return;
  const mode = modeRes.value;

  let multiCount = 2;
  if (mode === "multi") {
    const cntRes = await dialog.select({
      title: "图像数量",
      message: "要绘制几条图像?",
      items: [
        { label: "2条", value: 2 },
        { label: "3条", value: 3 }
      ],
      value: 2
    });
    if (cntRes.action !== "confirm") return;
    multiCount = cntRes.value;
  }

  const CURVE_COLORS = ['#ff4444', '#44ddff', '#88ff44'];

  async function inputEquations(count, defaults) {
    const out = [];
    for (let i = 0; i < count; i++) {
      const title = count === 1 ? "输入方程" : `输入第 ${i + 1} 条方程`;
      const message = (count === 1)
        ? "支持 y=f(x) 与圆锥曲线,例如:\ny=x\nx^2+y^2=25\nx^2/9+y^2/4=1"
        : undefined;
      const r = await dialog.text({
        title: title,
        message: message,
        value: defaults[i],
        placeholder: "y = ... 或 x^2+y^2=25",
        maxLength: 256
      });
      if (r.action !== "confirm") return null;
      const eq = r.value.trim();
      if (eq.indexOf("=") < 0) {
        await dialog.alert({ title: "格式错误", message: `第 ${i + 1} 条必须包含等号 =` });
        return null;
      }
      if (!buildEvaluator(eq)) {
        await dialog.alert({ title: "无法解析", message: `第 ${i + 1} 条方程无法解析` });
        return null;
      }
      out.push(eq);
    }
    return out;
  }

  const defaults = ["y=x", "y=-x", "y=0"];
  const initialEqs = await inputEquations(
    mode === "single" ? 1 : multiCount,
    defaults
  );
  if (!initialEqs) return;

  const xRangeRes = await dialog.number({
    title: "X轴正负范围", value: 5, min: 1, max: 30, decimals: 1
  });
  if (xRangeRes.action !== "confirm") return;
  const xMax = xRangeRes.value;

  const yRangeRes = await dialog.number({
    title: "Y轴正负范围", value: 5, min: 1, max: 30, decimals: 1
  });
  if (yRangeRes.action !== "confirm") return;
  const yMax = yRangeRes.value;

  // ==================== 状态 ====================
  const state = ui.signal({
    eqs: initialEqs,
    xRange: xMax,
    yRange: yMax,
    computing: false,
    computed: null,
    gen: 0
  });

  ui.setTitle("方程图像绘制器");
  ui.showHeader(true);

  async function promptEquations() {
    const cur = state.get();
    return await inputEquations(cur.eqs.length, cur.eqs);
  }

  // ==================== 异步分步计算 ====================
  function tick() {
    return new Promise(function (resolve) { setTimeout(resolve, 0); });
  }

  async function recompute(gen) {
    try {
      const s = state.get();
      if (s.gen !== gen) return;

      const GW = 280;
      const GH = 280;
      const OX = GW / 2;
      const OY = GH / 2;
      const scale = Math.min(OX / s.xRange, OY / s.yRange);
      const visX = OX / scale;
      const visY = OY / scale;

      const curves = [];
      for (let i = 0; i < s.eqs.length; i++) {
        const c = buildCurve(s.eqs[i], CURVE_COLORS[i % CURVE_COLORS.length]);
        if (c) curves.push(c);
      }

      await tick();
      if (state.get().gen !== gen) return;

      // 采样:STEP=4,点 6px,视觉平滑且性能可控
      const IMPLICIT_STEP = 4;
      const DOT_SIZE = 6;
      const EXP_SAMPLES = curves.length <= 1 ? 120 : (curves.length === 2 ? 100 : 80);
      const EXP_DOT = 5;

      const curvesData = [];
      for (let i = 0; i < curves.length; i++) {
        const c = curves[i];
        let points, dotSize;
        if (c.ev.mode === 'explicit') {
          points = sampleExplicitCurve(c.ev.f, visX, GW, GH, OX, OY, scale, EXP_SAMPLES);
          dotSize = EXP_DOT;
        } else {
          points = sampleImplicitCurve(c.F, GW, GH, OX, OY, scale, IMPLICIT_STEP);
          dotSize = DOT_SIZE;
        }
        curvesData.push({ points: points, color: c.color, dotSize: dotSize });

        await tick();
        if (state.get().gen !== gen) return;
      }

      // 坐标轴交点
      const axisResults = [];
      for (let i = 0; i < curves.length; i++) {
        const F = curves[i].F;
        const gx = function (x) { return F(x, 0); };
        const xs = findRoots1D(gx, -visX, visX, 80);
        const xPts = [];
        for (let k = 0; k < xs.length && k < 6; k++) {
          if (Math.abs(xs[k]) > visX) continue;
          xPts.push({ x: xs[k], y: 0 });
        }
        if (xPts.length > 0) {
          axisResults.push({ label: `图像${i + 1} 与 x 轴`, pts: xPts });
        }

        const gy = function (y) { return F(0, y); };
        const ys = findRoots1D(gy, -visY, visY, 80);
        const yPts = [];
        for (let k = 0; k < ys.length && k < 6; k++) {
          if (Math.abs(ys[k]) > visY) continue;
          yPts.push({ x: 0, y: ys[k] });
        }
        if (yPts.length > 0) {
          axisResults.push({ label: `图像${i + 1} 与 y 轴`, pts: yPts });
        }

        await tick();
        if (state.get().gen !== gen) return;
      }

      // 图像间交点
      const pairResults = [];
      for (let i = 0; i < curves.length; i++) {
        for (let j = i + 1; j < curves.length; j++) {
          const pts = findIntersections(curves[i], curves[j], visX, visY, OX, OY, scale, GW, GH);
          const valid = [];
          for (let k = 0; k < pts.length && k < 4; k++) {
            const p = pts[k];
            if (p.x < -visX || p.x > visX || p.y < -visY || p.y > visY) continue;
            valid.push(p);
          }
          if (valid.length > 0) {
            pairResults.push({ label: `图像${i + 1} ∩ 图像${j + 1}`, pts: valid });
          }

          await tick();
          if (state.get().gen !== gen) return;
        }
      }

      state.update(function (old) {
        return {
          ...old,
          computed: {
            curvesData: curvesData,
            axisResults: axisResults,
            pairResults: pairResults,
            GW: GW, GH: GH, OX: OX, OY: OY,
            scale: scale, visX: visX, visY: visY
          },
          computing: false
        };
      });
    } catch (e) {
      console.error('计算失败', e);
      state.update(function (old) { return { ...old, computing: false }; });
    }
  }

  function requestRecompute() {
    const gen = state.get().gen + 1;
    state.update(function (old) { return { ...old, gen: gen, computing: true, computed: null }; });
    recompute(gen);
  }

  // ==================== 渲染 ====================
  function buildUi() {
    const s = state.get();
    const children = [];

    if (s.eqs.length === 1) {
      children.push(ui.text("方程:" + s.eqs[0], {
        size: 16, lineHeight: 20, lines: 2, color: "#ffffff"
      }));
    } else {
      children.push(ui.text("方程列表:", {
        size: 16, lineHeight: 20, lines: 1, color: "#ffffff"
      }));
      for (let i = 0; i < s.eqs.length; i++) {
        children.push(ui.text(
          `图像${i + 1}:${s.eqs[i]}`,
          { size: 14, lineHeight: 18, lines: 2, color: CURVE_COLORS[i % CURVE_COLORS.length] }
        ));
      }
    }

    children.push(ui.spacer(4));

    if (s.computing || !s.computed) {
      children.push(ui.stack([], {
        width: 280, height: 280, background: "#000000", radius: 4
      }));
      children.push(ui.spacer(4));
      children.push(ui.text("⏳ 正在计算,请稍候…", {
        size: 18, lineHeight: 22, lines: 1, color: "#89ff89", align: "center"
      }));
    } else {
      const d = s.computed;
      const nodes = [];
      nodes.push(ui.stack([], { width: d.GW, height: d.GH, background: "#000000", radius: 4 }));
      nodes.push(ui.stack([], { x: 0, y: d.OY - 1, width: d.GW, height: 2, background: "#39ff39" }));
      nodes.push(ui.stack([], { x: d.OX - 1, y: 0, width: 2, height: d.GH, background: "#39ff39" }));

      const stepX = niceStep(d.visX);
      const startX = -Math.floor(d.visX / stepX) * stepX;
      for (let v = startX; v <= d.visX + 1e-9; v += stepX) {
        const px = Math.round(d.OX + v * d.scale);
        if (px < 0 || px > d.GW) continue;
        nodes.push(ui.stack([], { x: px - 1, y: d.OY - 5, width: 2, height: 10, background: "#39ff39" }));
        if (Math.abs(v) > 1e-9) {
          const lx = Math.max(0, Math.min(d.GW - 40, px - 20));
          nodes.push(ui.text(fmtNum(v), {
            x: lx, y: d.OY + 7, width: 40, height: 18,
            size: 16, lineHeight: 16, lines: 1, color: "#89ff89", align: "center"
          }));
        }
      }

      const stepY = niceStep(d.visY);
      const startY = -Math.floor(d.visY / stepY) * stepY;
      for (let v = startY; v <= d.visY + 1e-9; v += stepY) {
        const py = Math.round(d.OY - v * d.scale);
        if (py < 0 || py > d.GH) continue;
        nodes.push(ui.stack([], { x: d.OX - 5, y: py - 1, width: 10, height: 2, background: "#39ff39" }));
        if (Math.abs(v) > 1e-9) {
          nodes.push(ui.text(fmtNum(v), {
            x: d.OX - 48, y: py - 9, width: 40, height: 18,
            size: 16, lineHeight: 16, lines: 1, color: "#89ff89", align: "right"
          }));
        }
      }

      nodes.push(ui.text("O", {
        x: d.OX + 4, y: d.OY + 4, width: 18, height: 18,
        size: 16, lineHeight: 16, lines: 1, color: "#ffffff"
      }));

      // 曲线点
      for (let ci = 0; ci < d.curvesData.length; ci++) {
        const cd = d.curvesData[ci];
        const dotSize = cd.dotSize;
        const half = Math.floor(dotSize / 2);
        for (let k = 0; k < cd.points.length; k++) {
          const p = cd.points[k];
          const cx = p.x - half < 0 ? 0 : (p.x - half > d.GW - dotSize ? d.GW - dotSize : p.x - half);
          const cy = p.y - half < 0 ? 0 : (p.y - half > d.GH - dotSize ? d.GH - dotSize : p.y - half);
          nodes.push(ui.stack([], {
            x: cx, y: cy, width: dotSize, height: dotSize,
            background: cd.color, radius: half
          }));
        }
      }

      // 交点标记
      for (let i = 0; i < d.pairResults.length; i++) {
        const pts = d.pairResults[i].pts;
        for (let k = 0; k < pts.length; k++) {
          const px = d.OX + pts[k].x * d.scale;
          const py = d.OY - pts[k].y * d.scale;
          const cx = Math.max(0, Math.min(d.GW - 8, Math.round(px) - 4));
const cy = Math.max(0, Math.min(d.GH - 8, Math.round(py) - 4));
          nodes.push(ui.stack([], {
            x: cx, y: cy, width: 8, height: 8,
            background: "#ffff44", radius: 4
          }));
        }
      }

      for (let i = 0; i < d.axisResults.length; i++) {
        const pts = d.axisResults[i].pts;
        for (let k = 0; k < pts.length; k++) {
          const px = d.OX + pts[k].x * d.scale;
          const py = d.OY - pts[k].y * d.scale;
          const cx = Math.max(0, Math.min(d.GW - 8, Math.round(px) - 4));
          const cy = Math.max(0, Math.min(d.GH - 8, Math.round(py) - 4));
          nodes.push(ui.stack([], {
            x: cx, y: cy, width: 8, height: 8,
            background: "#ff88ff", radius: 4
          }));
        }
      }

      children.push(ui.stack(nodes, { width: d.GW, height: d.GH }));
      children.push(ui.spacer(4));

      if (d.axisResults.length > 0) {
        children.push(ui.text("与坐标轴交点:", {
          size: 16, lineHeight: 20, lines: 1, color: "#ff88ff"
        }));
        for (let i = 0; i < d.axisResults.length; i++) {
          const r = d.axisResults[i];
          const parts = [];
          for (let k = 0; k < r.pts.length; k++) {
            parts.push(`(${fmtNum(r.pts[k].x)}, ${fmtNum(r.pts[k].y)})`);
          }
          children.push(ui.text(r.label + ":" + parts.join("  "), {
            size: 14, lineHeight: 18, lines: 3, color: "#ffaaff"
          }));
        }
      }

      if (d.pairResults.length > 0) {
        children.push(ui.text("图像交点:", {
          size: 16, lineHeight: 20, lines: 1, color: "#ffff44"
        }));
        for (let i = 0; i < d.pairResults.length; i++) {
          const r = d.pairResults[i];
          const parts = [];
          for (let k = 0; k < r.pts.length; k++) {
            parts.push(`(${fmtNum(r.pts[k].x)}, ${fmtNum(r.pts[k].y)})`);
          }
          children.push(ui.text(r.label + ":" + parts.join("  "), {
            size: 14, lineHeight: 18, lines: 3, color: "#ffff88"
          }));
        }
      }
    }

    children.push(ui.spacer(4));

    const busy = s.computing;

    children.push(ui.button("重新输入方程", async () => {
      if (state.get().computing) return;
      try {
        const newEqs = await promptEquations();
        if (newEqs) {
          state.update(function (old) { return { ...old, eqs: newEqs }; });
          requestRecompute();
        }
      } catch (e) { console.error(e); }
    }, { id: "btnReInput", tone: "primary", disabled: busy }));

    children.push(ui.button("修改 X/Y 范围", async () => {
      if (state.get().computing) return;
      try {
        const cur = state.get();
        const rx = await dialog.number({
          title: "X正负范围", value: cur.xRange, min: 1, max: 30, decimals: 1
        });
        if (rx.action !== "confirm") return;
        const ry = await dialog.number({
          title: "Y正负范围", value: cur.yRange, min: 1, max: 30, decimals: 1
        });
        if (ry.action !== "confirm") return;
        state.update(function (old) {
          return { ...old, xRange: rx.value, yRange: ry.value };
        });
        requestRecompute();
      } catch (e) { console.error(e); }
    }, { id: "btnRange", tone: "neutral", disabled: busy }));

    return ui.column(children, { padding: 8, gap: 6 });
  }

  ui.render(buildUi);
  requestRecompute();
}
return main();