FormulaScope · 公式视界

让抽象的方程拥有形态。支持显式、隐式与不等式三类曲线,自动捕捉极值、轴交与曲线交点,并可实时绘制一阶导数与定积分轨迹。自适应取景、点阵缩放、多曲线同框,从代数到几何,一眼看穿。

适配设备:Pro

作者:liuwuxin

下载量:3 · 点赞:0

可信源码风险提示:保存或下载前请阅读源码和权限声明。
SHA-2566fb7181b0de8e987fadc57502801bc20e6a4e897d6fa5eddc1a3763690174fa2
async function main(){
const MF={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 MC={pi:Math.PI,PI:Math.PI,e:Math.E,E:Math.E,LN2:Math.LN2,LN10:Math.LN10,SQRT2:Math.SQRT2};
function sm(n){return n.indexOf('Math.')===0?n.slice(5):n}

function tk(src){
const o=[];let i=0;
const dg=c=>c>='0'&&c<='9';
const al=c=>(c>='a'&&c<='z')||(c>='A'&&c<='Z')||c==='_';
while(i<src.length){
const c=src[i];
if(' \t\n\r'.indexOf(c)>=0){i++;continue}
if(dg(c)||(c==='.'&&dg(src[i+1]))){
let j=i;
while(j<src.length&&(dg(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&&dg(src[k])){while(k<src.length&&dg(src[k]))k++;j=k}
}
o.push({t:'num',v:parseFloat(src.slice(i,j))});i=j;continue
}
if(al(c)){let j=i;while(j<src.length&&(al(src[j])||dg(src[j])||src[j]==='.'))j++;o.push({t:'id',v:src.slice(i,j)});i=j;continue}
if(c==='>'||c==='<'){if(src[i+1]==='='){o.push({t:'op',v:c+'='});i+=2}else{o.push({t:'op',v:c});i++}continue}
if('+-*/%^(),'.indexOf(c)>=0){o.push({t:'op',v:c});i++;continue}
throw new Error('c '+c)
}
const r=[];let p=null;
for(const c of o){
if(p){
const pv=p.t==='id'?sm(p.v):'';
const isFn=p.t==='id'&&Object.prototype.hasOwnProperty.call(MF,pv);
if((p.t==='num'&&(c.t==='id'||(c.t==='op'&&c.v==='(')))||
(p.t==='id'&&c.t==='num')||
(p.t==='id'&&(c.t==='op'&&c.v==='(')&&!isFn)||
(p.t==='op'&&p.v===')'&&(c.t==='id'||c.t==='num'||(c.t==='op'&&c.v==='('))))
r.push({t:'op',v:'*'})
}
r.push(c);p=c
}
return r
}

function ps(tkns){
let pos=0;
function isOp(v){const t=tkns[pos];return t&&t.t==='op'&&t.v===v}
function eo(v){if(isOp(v)){pos++;return true}return false}
function pe(){return pa()}
function pa(){let n=pm();while(true){const t=tkns[pos];if(t&&t.t==='op'&&(t.v==='+'||t.v==='-')){pos++;n={k:'bin',op:t.v,l:n,r:pm()}}else break}return n}
function pm(){let n=pu();while(true){const t=tkns[pos];if(t&&t.t==='op'&&(t.v==='*'||t.v==='/'||t.v==='%')){pos++;n={k:'bin',op:t.v,l:n,r:pu()}}else break}return n}
function pu(){const t=tkns[pos];if(t&&t.t==='op'&&(t.v==='-'||t.v==='+')){pos++;const a=pu();return t.v==='-'?{k:'neg',a:a}:a}return pp()}
function pp(){const b=pat();if(isOp('^')){pos++;return{k:'bin',op:'^',l:b,r:pu()}}return b}
function pat(){
const t=tkns[pos];if(!t)throw new Error('end');
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(pe());while(eo(','))args.push(pe())}if(!eo(')'))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=pe();if(!eo(')'))throw new Error(')');return e}
throw new Error('u '+t.v)
}
const a=pe();if(pos!==tkns.length)throw new Error('x');return a
}

function ev(n,x,y){
switch(n.k){
case'num':return n.v;
case'var':{
const nm=n.name;if(nm==='x'||nm==='X')return x;if(nm==='y'||nm==='Y')return y;
const k=sm(nm);if(Object.prototype.hasOwnProperty.call(MC,k))return MC[k];
throw new Error('v '+nm)
}
case'neg':return-ev(n.a,x,y);
case'bin':{
const a=ev(n.l,x,y),b=ev(n.r,x,y);
if(n.op==='+')return a+b;if(n.op==='-')return a-b;if(n.op==='*')return a*b;
if(n.op==='/')return a/b;if(n.op==='%')return a%b;if(n.op==='^')return Math.pow(a,b);
throw new Error('o')
}
case'call':{
const k=sm(n.name);const f=MF[k];if(!f)throw new Error('f '+n.name);
return f.apply(Math,n.args.map(a=>ev(a,x,y)))
}
}
throw new Error('n')
}

function hv(n,m){
if(!n)return false;
if(n.k==='var')return n.name===m||n.name===m.toUpperCase();
if(n.k==='neg')return hv(n.a,m);
if(n.k==='bin')return hv(n.l,m)||hv(n.r,m);
if(n.k==='call')return n.args.some(a=>hv(a,m));
return false
}

function be(e){
for(let i=0;i<e.length;i++){
const c=e[i];
if(c==='>'||c==='<'){
let op=c,ol=1;if(e[i+1]==='='){op=c+'=';ol=2}
try{
const L=ps(tk(e.slice(0,i)));const R=ps(tk(e.slice(i+ol)));
const F=(x,y)=>ev(L,x,y)-ev(R,x,y);
if(typeof F(1,1)!=='number')return null;
return{mode:'ineq',F:F,op:op}
}catch(x){return null}
}
}
const i=e.indexOf('=');if(i<0)return null;
try{
const L=ps(tk(e.slice(0,i)));const R=ps(tk(e.slice(i+1)));
const ly=L.k==='var'&&(L.name==='y'||L.name==='Y');
if(ly&&!hv(R,'y')){
const f=x=>ev(R,x,0);
if(typeof f(1)!=='number')return null;
return{mode:'explicit',f:f}
}
const F=(x,y)=>ev(L,x,y)-ev(R,x,y);
if(typeof F(1,1)!=='number')return null;
const vs=[];
for(const xx of[0,1,-1,2.5])for(const yy of[0,1,-1,2.5]){
try{const vv=F(xx,yy);if(isFinite(vv))vs.push(vv)}catch(x){}
}
if(vs.length===0)return null;
if(vs.every(v=>Math.abs(v)<1e-12))return{mode:'trivial',msg:'恒等式,无意义'};
if(vs.every(v=>Math.abs(v-vs[0])<1e-9*(1+Math.abs(vs[0]))))return{mode:'trivial',msg:'无解(矛盾式)'};
return{mode:'implicit',F:F}
}catch(x){return null}
}

function bc(e,c){
const v=be(e);if(!v)return null;
if(v.mode==='ineq')return{eq:e,ev:v,F:v.F,color:c,isIneq:true,op:v.op};
if(v.mode==='trivial')return{eq:e,ev:v,F:null,color:c,isIneq:false,op:null,trivial:true};
const F=v.mode==='explicit'?((x,y)=>y-v.f(x)):v.F;
return{eq:e,ev:v,F:F,color:c,isIneq:false,op:null}
}

function ns(r){
const t=r/3;const a=[1,2,5,10,15,20,30,50,100];
for(let i=0;i<a.length;i++)if(t<=a[i])return a[i];
return 100
}

function fm(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
}

function mv(s,w,h){
const ox=w/2,oy=h/2;
const scx=ox/s.xRange,scy=oy/s.yRange;
const sc=Math.min(scx,scy);
const vx=ox/sc,vy=oy/sc;
return{GW:w,GH:h,OX:ox,OY:oy,scale:sc,cx:s.cx,cy:s.cy,visX:vx,visY:vy,
toPx:x=>ox+(x-s.cx)*sc,toPy:y=>oy-(y-s.cy)*sc,
toX:px=>s.cx+(px-ox)/sc,toY:py=>s.cy-(py-oy)/sc}
}

function dv(f){
const h=1e-5;
return x=>{const a=f(x+h),b=f(x-h);if(!isFinite(a)||!isFinite(b))return NaN;return(a-b)/(2*h)}
}

function fe(f,a,b,N){
const st=(b-a)/N,o=[];
function sf(x){try{const v=f(x);return(typeof v==='number'&&isFinite(v))?v:NaN}catch(e){return NaN}}
let y2=NaN,y1=sf(a),x1=a;
for(let i=1;i<=N;i++){
const x=a+i*st,y=sf(x);
if(isFinite(y2)&&isFinite(y1)&&isFinite(y)){
if(y1>y2&&y1>y)o.push({x:x1,y:y1,kind:'max'});
else if(y1<y2&&y1<y)o.push({x:x1,y:y1,kind:'min'})
}
y2=y1;y1=y;x1=x
}
return o
}

function fr(g,a,b,N){
const st=(b-a)/N,rs=[];
function sf(x){try{const v=g(x);return(typeof v==='number'&&isFinite(v))?v:NaN}catch(e){return NaN}}
let px=a,py=sf(a);
if(isFinite(py)&&Math.abs(py)<1e-7)rs.push(a);
let prev2=NaN;
for(let i=1;i<=N;i++){
const x=a+i*st,y=sf(x);
if(isFinite(y)){
if(Math.abs(y)<1e-7)rs.push(x);
else if(isFinite(py)&&Math.abs(py)>=1e-7&&((py>0)!==(y>0))){
let lo=px,hi=x,fa=py;
for(let k=0;k<60;k++){
const m=(lo+hi)*0.5,fv=sf(m);
if(!isFinite(fv))break;
if(fv===0){lo=m;hi=m;break}
if((fa>0)!==(fv>0))hi=m;else{lo=m;fa=fv}
}
const rt=(lo+hi)*0.5,gv=sf(rt);
if(isFinite(gv)&&Math.abs(gv)<1e-2)rs.push(rt)
}
if(isFinite(prev2)&&isFinite(py)&&isFinite(y)&&Math.abs(py)<1e-3){
if((py<prev2&&py<y)||(py>prev2&&py>y))rs.push(px)
}
}
prev2=py;px=x;py=y
}
rs.sort((a,b)=>a-b);
const u=[],mg=st*0.5;
for(let i=0;i<rs.length;i++)if(i===0||Math.abs(rs[i]-rs[i-1])>mg)u.push(rs[i]);
return u
}

function nw(F1,F2,x0,y0){
let x=x0,y=y0;const e=1e-7;
for(let k=0;k<30;k++){
let f1,f2;
try{f1=F1(x,y);f2=F2(x,y)}catch(er){return null}
if(!isFinite(f1)||!isFinite(f2))return null;
if(Math.abs(f1)<1e-11&&Math.abs(f2)<1e-11)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(er){return null}
if(!isFinite(f1x)||!isFinite(f1y)||!isFinite(f2x)||!isFinite(f2y))return null;
const dt=f1x*f2y-f1y*f2x;
if(Math.abs(dt)<1e-15)return null;
const dx=(f1*f2y-f1y*f2)/dt,dy=(f1x*f2-f1*f2x)/dt;
x-=dx;y-=dy;
if(Math.abs(dx)<1e-10&&Math.abs(dy)<1e-10)break
}
return{x:x,y:y}
}

function ar(f){
const N=300,xs=[],ys=[];
for(let i=0;i<N;i++){
const x=-20+40*i/(N-1);let y;
try{y=f(x)}catch(e){continue}
if(isFinite(y)){xs.push(x);ys.push(y)}
}
if(ys.length===0)return{xMin:-5,xMax:5,yMin:-5,yMax:5};
const ysS=ys.slice().sort((a,b)=>a-b);
let yL=ysS[Math.floor(ys.length*0.05)],yH=ysS[Math.floor(ys.length*0.95)];
if(yL>0)yL=0;if(yH<0)yH=0;
if(yH-yL<1e-6){yL-=1;yH+=1}
const Ps=[Math.PI/6,Math.PI/4,Math.PI/3,Math.PI/2,Math.PI,2*Math.PI,1,2,3,4,5,6,10];
let P=null;
for(const pp of Ps){
let ok=true;
for(let i=0;i<15;i++){
const x=-3+6*i/14;let a,b;
try{a=f(x);b=f(x+pp)}catch(e){ok=false;break}
if(!isFinite(a)||!isFinite(b))continue;
if(Math.abs(a-b)>1e-3*(1+Math.abs(a))){ok=false;break}
}
if(ok){P=pp;break}
}
let xM,xX;
if(P){const s=Math.max(P*2,1);xM=-s;xX=s}
else{
const zs=[];
for(let i=1;i<xs.length;i++){
if(ys[i-1]*ys[i]<0){
let a=xs[i-1],b=xs[i],fa=ys[i-1];
for(let k=0;k<40;k++){
const m=(a+b)/2,fv=f(m);
if(!isFinite(fv))break;
if(fa*fv<=0)b=m;else{a=m;fa=fv}
}
zs.push((a+b)/2)
}
}
if(zs.length>0){const mn=zs[0],mx=zs[zs.length-1];const pd=Math.max((mx-mn)*0.3,1);xM=mn-pd;xX=mx+pd}
else{
const xS=xs.slice().sort((a,b)=>a-b);
xM=Math.min(xS[Math.floor(xS.length*0.05)],-1);
xX=Math.max(xS[Math.floor(xS.length*0.95)],1)
}
}
if(xX-xM>100){const c=(xM+xX)/2;xM=c-50;xX=c+50}
if(xX-xM<1){const c=(xM+xX)/2;xM=c-1;xX=c+1}
const pd=(xX-xM)*0.15;xM-=pd;xX+=pd;
const yp=(yH-yL)*0.15;yL-=yp;yH+=yp;
const sp=Math.max(xX-xM,yH-yL);
const xc=(xM+xX)/2,yc=(yL+yH)/2;
xM=xc-sp/2;xX=xc+sp/2;yL=yc-sp/2;yH=yc+sp/2;
return{xMin:xM,xMax:xX,yMin:yL,yMax:yH}
}

const KP=[
[['x','y','(',')','='],['7','8','9','/','^'],['4','5','6','*','-'],['1','2','3','+','.'],['0','e','π','←','C'],['⇄','sin','cos','✗','✓']],
[['x','y','(',')','='],['log','ln','exp','pow','abs'],['asin','acos','atan','sqrt','cbrt'],['min','max','round','floor','ceil'],['<','>','<=','>=','tan'],['⇄','sin','cos','✗','✓']]
];
const KW=49,KH=34,KG=4;
let kl=false;

const st=ui.signal({phase:'mode',mode:'single',multiCount:1,eqs:[],editIndex:0,editText:'',editPage:0,reEdit:false,xRange:5,yRange:5,initX:5,initY:5,cx:0,cy:0,densityLevel:1,showDeriv:false,showInteg:false,progress:1,warn:''});
ui.setTitle('方程图像');
ui.showHeader(true);

const DN=[80,200,500];
const CC=['#ff4444','#44ddff','#88ff44'];
const DC='#c864ff',IC='#00ffff';
const IF=['#552222','#225555','#335522'];

function row(items,g){return ui.row(items,{gap:g||6})}
function btn(t,cb,i,tn,h){return ui.button(t,cb,{id:i,tone:tn||'neutral',height:h})}
function trow(txt,color,sz){return ui.text(txt,{size:sz||14,lineHeight:(sz||14)+4,lines:1,color:color})}
function tcol(txt,color,sz,ln){return ui.text(txt,{size:sz||14,lineHeight:(sz||14)+4,lines:ln||3,color:color})}

function dp(pts,v,ns2,c,D,maxN){
if(!pts||pts.length===0)return;
const dr={},h=Math.floor(D/2);
const lim=maxN||600;
const step=pts.length>lim?Math.ceil(pts.length/lim):1;
for(let i=0;i<pts.length;i+=step){
const p=pts[i];
const px=v.toPx(p.x),py=v.toPy(p.y);
if(px<-3||px>v.GW+3||py<-3||py>v.GH+3)continue;
const ix=Math.round(px),iy=Math.round(py);
const k=ix+':'+iy;
if(dr[k])continue;
dr[k]=1;
ns2.push(ui.stack([],{x:Math.max(0,Math.min(v.GW-D,ix-h)),y:Math.max(0,Math.min(v.GH-D,iy-h)),width:D,height:D,background:c,radius:2}));
}
if(step>1){
const p=pts[pts.length-1];
const px=v.toPx(p.x),py=v.toPy(p.y);
const ix=Math.round(px),iy=Math.round(py);
const k=ix+':'+iy;
if(!dr[k]){
dr[k]=1;
ns2.push(ui.stack([],{x:Math.max(0,Math.min(v.GW-D,ix-h)),y:Math.max(0,Math.min(v.GH-D,iy-h)),width:D,height:D,background:c,radius:2}));
}
}
}

let job=null,adp=false,adpTimer=null,pend=false,tickCount=0,vd=null;

function ck(s){
return JSON.stringify({eqs:s.eqs,xR:s.xRange,yR:s.yRange,cx:s.cx,cy:s.cy,dl:s.densityLevel,showDeriv:s.showDeriv,showInteg:s.showInteg})
}

function canInc(s){
if(s.showDeriv||s.showInteg)return false;
if(!job)return false;
if(job.stageIdx<1)return false;
if(job.density!==DN[s.densityLevel])return false;
for(let i=0;i<job.items.length;i++)if(job.items[i].kind!=='explicit')return false;
return true
}

function ext(it,nx0,nx1){
if(it.pts.length>5000)return false;
if(it.stepX<=0)return false;
const add=[];
if(nx0<it.sampledX0-it.stepX*0.5){
let x=it.sampledX0-it.stepX;
while(x>=nx0){
let y;try{y=it.curve.ev.f(x)}catch(e){y=NaN}
if(typeof y==='number'&&isFinite(y))add.push({x:x,y:y});
x-=it.stepX;
if(add.length>2000)return false
}
}
if(nx1>it.sampledX1+it.stepX*0.5){
let x=it.sampledX1+it.stepX;
while(x<=nx1){
let y;try{y=it.curve.ev.f(x)}catch(e){y=NaN}
if(typeof y==='number'&&isFinite(y))add.push({x:x,y:y});
x+=it.stepX;
if(add.length>2000)return false
}
}
if(add.length===0)return true;
it.sampledX0=nx0;it.sampledX1=nx1;
it.pts=it.pts.concat(add).sort((a,b)=>a.x-b.x);
return true
}

function sj(s,quiet){
if(job&&job.timer){clearInterval(job.timer);job.timer=null}
if(vd){clearTimeout(vd);vd=null}
if(adpTimer){clearTimeout(adpTimer);adpTimer=null}
adp=false;

const v=mv(s,280,280);
const dn=DN[s.densityLevel];
const cs=[];
for(let i=0;i<s.eqs.length;i++){const c=bc(s.eqs[i],CC[i%3]);if(c)cs.push(c)}

const its=cs.map(c=>({curve:c,kind:c.trivial?'trivial':(c.isIneq?'ineq':(c.ev.mode==='explicit'?'explicit':'implicit')),pts:[],total:0,cursor:0,gV:null,gO:null,gC:0,gR:0,gCu:0,bCu:0,stp:4,fillRects:null,fillRows:0,fillCu:0,fillDone:false,dPt:null,dCu:0,iPt:null,iCu:0,iAc:0,iPv:0,iDx:0,sampledX0:0,sampledX1:0,stepX:0}));

const x0=s.cx-v.visX,x1=s.cx+v.visX;

for(let i=0;i<its.length;i++){
const it=its[i];
if(it.kind==='explicit'){
it.total=dn;
it.sampledX0=x0;
it.sampledX1=x1;
it.stepX=(x1-x0)/dn;
if(quiet){
for(let k=0;k<dn;k++){
const x=x0+it.stepX*k;
let y;try{y=it.curve.ev.f(x)}catch(e){continue}
if(typeof y==='number'&&isFinite(y))it.pts.push({x:x,y:y});
}
it.cursor=dn
}
}
}

job={key:ck(s),view:v,density:dn,x0:x0,x1:x1,
snp:{cx:s.cx,cy:s.cy,showDeriv:s.showDeriv,showInteg:s.showInteg},
items:its,extremaResults:[],axisResults:[],pairResults:[],
stageIdx:quiet?1:0,
stages:[tickSample,tickImplicit,tickIneq,tickAux,tickExtrema,tickRoots,tickIntersections],
stExtremaIdx:0,stRootsIdx:0,stRootsAxis:0,stPairI:0,stPairJ:1,stPairData:null,
timer:null,allDone:false,quiet:!!quiet};

tickCount=0;
st.update(o=>({...o,progress:quiet?1:0}));
job.timer=setInterval(tickJob,50);
tickJob()
}

function tickJob(){
if(!job)return;
tickCount++;
const t0=Date.now();
while(Date.now()-t0<25&&job.stageIdx<job.stages.length){
const f=job.stages[job.stageIdx];
if(!f())job.stageIdx++;else break
}
const isEnd=job.stageIdx>=job.stages.length;
if(isEnd){
job.allDone=true;
if(job.timer){clearInterval(job.timer);job.timer=null}
st.update(o=>({...o,progress:1}));
return
}
if(!job.quiet&&tickCount%2===0){
st.update(o=>({...o,progress:Math.min(1,job.stageIdx/job.stages.length)}))
}
}

function tickSample(){
let any=false;
const FR=12;
for(let i=0;i<job.items.length;i++){
const it=job.items[i];
if(it.kind!=='explicit')continue;
if(it.cursor>=it.total)continue;
any=true;
const bt=Math.max(1,Math.ceil(it.total/FR));
const end=Math.min(it.total,it.cursor+bt);
for(let k=it.cursor;k<end;k++){
const x=job.x0+(job.x1-job.x0)*k/(it.total-1);
let y;try{y=it.curve.ev.f(x)}catch(e){continue}
if(typeof y==='number'&&isFinite(y))it.pts.push({x:x,y:y});
}
it.cursor=end
}
return any
}

function ig(it){
const ST=4;
if(!it.gV){
it.stp=ST;
it.gC=Math.floor(job.view.GW/ST)+2;
it.gR=Math.floor(job.view.GH/ST)+2;
it.gV=new Float64Array(it.gC*it.gR);
it.gO=new Uint8Array(it.gC*it.gR);
it.gCu=0;it.bCu=0
}
if(it.gCu<it.gR){
const bt=Math.max(1,Math.ceil(it.gR/20));
const end=Math.min(it.gR,it.gCu+bt);
for(let j=it.gCu;j<end;j++){
const py=j*it.stp;const yv=job.view.toY(py);
for(let i=0;i<it.gC;i++){
const px=i*it.stp;const xv=job.view.toX(px);
let vv;try{vv=it.curve.F(xv,yv)}catch(e){vv=NaN}
const idx=j*it.gC+i;
if(typeof vv==='number'&&isFinite(vv)){it.gV[idx]=vv;it.gO[idx]=1}
}
}
it.gCu=end;return true
}
const tc=(it.gR-1)*(it.gC-1);
if(it.bCu<tc){
const bt=Math.max(1,Math.ceil(tc/15));
const end=Math.min(tc,it.bCu+bt);
const F=it.curve.F;
for(let ci=it.bCu;ci<end;ci++){
const jj=Math.floor(ci/(it.gC-1));
const ii=ci%(it.gC-1);
const i00=jj*it.gC+ii,i10=jj*it.gC+ii+1,i01=(jj+1)*it.gC+ii,i11=(jj+1)*it.gC+ii+1;
if(!it.gO[i00]||!it.gO[i10]||!it.gO[i01]||!it.gO[i11])continue;
const v00=it.gV[i00],v10=it.gV[i10],v01=it.gV[i01],v11=it.gV[i11];
const p00=v00>0,p10=v10>0,p01=v01>0,p11=v11>0;
if(p00===p10&&p10===p01&&p01===p11)continue;
const px0=ii*it.stp,py0=jj*it.stp,px1=(ii+1)*it.stp,py1=(jj+1)*it.stp;
const x0=job.view.toX(px0),x1=job.view.toX(px1),y0=job.view.toY(py0),y1=job.view.toY(py1);
function bs(xa,ya,xb,yb,va){
let lo=0,hi=1;const vp=va>0;
for(let k=0;k<18;k++){
const m=(lo+hi)*0.5;
const mx=xa+(xb-xa)*m,my=ya+(yb-ya)*m;
let vm;try{vm=F(mx,my)}catch(e){break}
if(!isFinite(vm))break;
if((vm>0)===vp)lo=m;else hi=m
}
const t=(lo+hi)*0.5;
return{x:xa+(xb-xa)*t,y:ya+(yb-ya)*t}
}
if(p00!==p10)it.pts.push(bs(x0,y0,x1,y0,v00));
if(p01!==p11)it.pts.push(bs(x0,y1,x1,y1,v01));
if(p00!==p01)it.pts.push(bs(x0,y0,x0,y1,v00));
if(p10!==p11)it.pts.push(bs(x1,y0,x1,y1,v10))
}
it.bCu=end;return true
}
return false
}

function tickImplicit(){
let any=false;
for(let i=0;i<job.items.length;i++){
const it=job.items[i];
if(it.kind!=='implicit')continue;
if(ig(it))any=true
}
return any
}

function tickIneq(){
let any=false;
for(let i=0;i<job.items.length;i++){
const it=job.items[i];
if(it.kind!=='ineq')continue;
if(!it.fillDone){
if(!it.fillRects){it.fillRects=[];it.fillRows=Math.floor(job.view.GH/10)+1;it.fillCu=0}
if(it.fillCu<it.fillRows){
any=true;
const ST=10;
const bt=Math.max(1,Math.ceil(it.fillRows/20));
const end=Math.min(it.fillRows,it.fillCu+bt);
for(let r=it.fillCu;r<end;r++){
const py=r*ST;const yv=job.view.toY(py+ST/2);
for(let c=0;c<job.view.GW;c+=ST){
const xv=job.view.toX(c+ST/2);
let vv;try{vv=it.curve.F(xv,yv)}catch(e){continue}
if(!isFinite(vv))continue;
let mm=false;const op=it.curve.op;
if(op==='>')mm=vv>0;else if(op==='<')mm=vv<0;
else if(op==='>=')mm=vv>=0;else if(op==='<=')mm=vv<=0;
if(mm)it.fillRects.push({x:c,y:py})
}
}
it.fillCu=end;
continue
}
it.fillDone=true
}
if(ig(it))any=true
}
return any
}

function tickAux(){
let any=false;
const sn=job.snp;
for(let i=0;i<job.items.length;i++){
const it=job.items[i];
if(it.kind!=='explicit')continue;
if(sn.showDeriv&&it.dPt===null){it.dPt=[];it.dCu=0}
if(it.dPt&&it.dCu<it.total){
any=true;
const bt=Math.max(1,Math.ceil(it.total/20));
const end=Math.min(it.total,it.dCu+bt);
const df=dv(it.curve.ev.f);
for(let k=it.dCu;k<end;k++){
const x=job.x0+(job.x1-job.x0)*k/(it.total-1);
let y;try{y=df(x)}catch(e){continue}
if(typeof y==='number'&&isFinite(y))it.dPt.push({x:x,y:y})
}
it.dCu=end;continue
}
if(sn.showInteg&&it.iPt===null){
it.iPt=[{x:job.x0,y:0}];it.iCu=1;it.iAc=0;
try{const vv=it.curve.ev.f(job.x0);it.iPv=isFinite(vv)?vv:0}catch(e){it.iPv=0}
it.iDx=(job.x1-job.x0)/it.total
}
if(it.iPt&&it.iCu<it.total){
any=true;
const bt=Math.max(1,Math.ceil(it.total/20));
const end=Math.min(it.total,it.iCu+bt);
for(let k=it.iCu;k<end;k++){
const x=job.x0+(job.x1-job.x0)*k/(it.total-1);
let cur;try{cur=it.curve.ev.f(x)}catch(e){cur=it.iPv}
if(!isFinite(cur))cur=it.iPv;
it.iAc+=(it.iPv+cur)*0.5*it.iDx;
it.iPt.push({x:x,y:it.iAc});
it.iPv=cur
}
it.iCu=end;continue
}
}
return any
}

function tickExtrema(){
if(job.stExtremaIdx>=job.items.length)return false;
const it=job.items[job.stExtremaIdx];
if(it.kind!=='explicit'){job.stExtremaIdx++;return true}
const ex=fe(it.curve.ev.f,job.x0,job.x1,300);
const vd2=[];const sn=job.snp;
for(let k=0;k<ex.length&&vd2.length<6;k++){
if(ex[k].y<sn.cy-job.view.visY||ex[k].y>sn.cy+job.view.visY)continue;
vd2.push(ex[k])
}
if(vd2.length>0)job.extremaResults.push({idx:job.stExtremaIdx,pts:vd2});
job.stExtremaIdx++;return true
}

function tickRoots(){
if(job.stRootsIdx>=job.items.length)return false;
const it=job.items[job.stRootsIdx];
const c=it.curve;const sn=job.snp;
if(job.stRootsAxis===0){
const xs=fr(x=>c.F(x,0),sn.cx-job.view.visX,sn.cx+job.view.visX,200);
const xP=[];
for(let k=0;k<xs.length&&k<8;k++)xP.push({x:xs[k],y:0});
if(xP.length>0)job.axisResults.push({idx:job.stRootsIdx,label:`${job.stRootsIdx+1}-x`,pts:xP});
job.stRootsAxis=1;return true
}else{
const ys=fr(y=>c.F(0,y),sn.cy-job.view.visY,sn.cy+job.view.visY,200);
const yP=[];
for(let k=0;k<ys.length&&k<8;k++)yP.push({x:0,y:ys[k]});
if(yP.length>0)job.axisResults.push({idx:job.stRootsIdx,label:`${job.stRootsIdx+1}-y`,pts:yP});
job.stRootsAxis=0;job.stRootsIdx++;return true
}
}

function tickIntersections(){
const sn=job.snp;
while(job.stPairI<job.items.length){
if(job.stPairJ>=job.items.length){
job.stPairI++;job.stPairJ=job.stPairI+1;job.stPairData=null;continue
}
if(!job.stPairData){
const c1=job.items[job.stPairI].curve,c2=job.items[job.stPairJ].curve;
if(!c1.F||!c2.F){job.stPairData='skip';job.stPairJ++;continue}
job.stPairData={F1:c1.F,F2:c2.F,STEP:4,gCu:0,cCu:0,A:null,B:null,cols:0,rows:0,res:[],seen:{}}
}
const pd=job.stPairData;
if(pd==='skip'){job.stPairData=null;job.stPairJ++;continue}
if(!pd.A){
pd.cols=Math.floor(job.view.GW/pd.STEP)+2;
pd.rows=Math.floor(job.view.GH/pd.STEP)+2;
pd.A=new Float64Array(pd.cols*pd.rows);
pd.B=new Float64Array(pd.cols*pd.rows)
}
if(pd.gCu<pd.rows){
const end=Math.min(pd.rows,pd.gCu+Math.max(1,Math.ceil(pd.rows/15)));
for(let j=pd.gCu;j<end;j++){
const py=j*pd.STEP;const yv=job.view.toY(py);
for(let i=0;i<pd.cols;i++){
const px=i*pd.STEP;const xv=job.view.toX(px);
let v1=NaN,v2=NaN;
try{v1=pd.F1(xv,yv)}catch(e){}
try{v2=pd.F2(xv,yv)}catch(e){}
pd.A[j*pd.cols+i]=v1;pd.B[j*pd.cols+i]=v2
}
}
pd.gCu=end;return true
}
const tc2=(pd.rows-1)*(pd.cols-1);
if(pd.cCu<tc2){
const end=Math.min(tc2,pd.cCu+Math.max(1,Math.ceil(tc2/10)));
for(let ci=pd.cCu;ci<end;ci++){
const jj=Math.floor(ci/(pd.cols-1)),ii=ci%(pd.cols-1);
const i00=jj*pd.cols+ii,i10=jj*pd.cols+ii+1,i01=(jj+1)*pd.cols+ii,i11=(jj+1)*pd.cols+ii+1;
const a00=pd.A[i00],a10=pd.A[i10],a01=pd.A[i01],a11=pd.A[i11];
const b00=pd.B[i00],b10=pd.B[i10],b01=pd.B[i01],b11=pd.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,pb=b00>0;
const sa=((a10>0)!==pa)||((a01>0)!==pa)||((a11>0)!==pa);
const sb=((b10>0)!==pb)||((b01>0)!==pb)||((b11>0)!==pb);
if(!sa||!sb)continue;
const sol=nw(pd.F1,pd.F2,job.view.toX(ii*pd.STEP+pd.STEP*0.5),job.view.toY(jj*pd.STEP+pd.STEP*0.5));
if(!sol||!isFinite(sol.x)||!isFinite(sol.y))continue;
let f1,f2;
try{f1=pd.F1(sol.x,sol.y);f2=pd.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;
const k=Math.round(sol.x*2000)+':'+Math.round(sol.y*2000);
if(pd.seen[k])continue;
pd.seen[k]=1;pd.res.push(sol)
}
pd.cCu=end;return true
}
pd.res.sort((a,b)=>a.x-b.x);
const vd2=[];
for(let k=0;k<pd.res.length;k++){
const p=pd.res[k];
if(p.x<sn.cx-job.view.visX||p.x>sn.cx+job.view.visX)continue;
if(p.y<sn.cy-job.view.visY||p.y>sn.cy+job.view.visY)continue;
vd2.push(p)
}
if(vd2.length>0)job.pairResults.push({label:`${job.stPairI+1}∩${job.stPairJ+1}`,pts:vd2});
job.stPairData=null;job.stPairJ++;return true
}
return false
}

function vch(s){
if(!job){sj(s,true);return}
const nv=mv(s,280,280);
const nx0=s.cx-nv.visX,nx1=s.cx+nv.visX;
const oldW=job.x1-job.x0,newW=nx1-nx0;

job.view=nv;
job.x0=nx0;job.x1=nx1;
job.snp.cx=s.cx;job.snp.cy=s.cy;
job.key=ck(s);
job.allDone=false;

if(canInc(s)&&Math.abs(oldW-newW)<0.001){
let ok=true;
for(let i=0;i<job.items.length;i++){
if(!ext(job.items[i],nx0,nx1)){ok=false;break}
}
if(ok){
job.stageIdx=4;
job.quiet=true;
job.stExtremaIdx=0;job.stRootsIdx=0;job.stRootsAxis=0;
job.stPairI=0;job.stPairJ=1;job.stPairData=null;
job.extremaResults=[];job.axisResults=[];job.pairResults=[];
if(job.timer){clearInterval(job.timer);job.timer=null}
if(vd){clearTimeout(vd);vd=null}
vd=setTimeout(()=>{
vd=null;
if(!job)return;
if(job.key!==ck(st.get()))return;
job.timer=setInterval(tickJob,50);
tickJob()
},300);
st.update(o=>({...o,progress:1}));
return
}
}
sj(s,true)
}

function zm(f){
st.update(o=>({...o,xRange:Math.max(0.5,Math.min(500,o.xRange*f)),yRange:Math.max(0.5,Math.min(500,o.yRange*f))}));
vch(st.get())
}

function pn(fx,fy){
st.update(o=>{
const sc=Math.min(140/o.xRange,140/o.yRange);
const vx=140/sc,vy=140/sc;
return{...o,cx:o.cx+fx*vx*0.3,cy:o.cy+fy*vy*0.3}
});
vch(st.get())
}

function rv(){
st.update(o=>({...o,xRange:o.initX,yRange:o.initY,cx:0,cy:0}));
vch(st.get())
}

function aa(){
const s=st.get();
const cs=s.eqs.map((e,i)=>bc(e,CC[i%3])).filter(Boolean);
const ex=cs.find(c=>c.ev.mode==='explicit');
if(!ex){dialog.alert({title:'!',message:'仅支持y=f(x)'});return}
const r=ar(ex.ev.f);
const xc=(r.xMin+r.xMax)/2,yc=(r.yMin+r.yMax)/2;
const hr=Math.max(0.5,(r.xMax-r.xMin)/2);
st.update(o=>({...o,cx:xc,cy:yc,xRange:hr,yRange:hr,densityLevel:1}));
sj(st.get(),false)
}

function hk(k){
if(kl)return;
const s=st.get();
if(k==='✗'){
if(s.reEdit){
st.update(o=>({...o,phase:'draw',reEdit:false,editText:'',editPage:0}));
vch(st.get())
}else{
st.update(o=>({...o,phase:'mode',eqs:[],editIndex:0,editText:'',editPage:0,reEdit:false}))
}
return
}
if(k==='⇄'){st.update(o=>({...o,editPage:1-o.editPage}));return}
if(k==='C'){st.update(o=>({...o,editText:''}));return}
if(k==='←'){st.update(o=>({...o,editText:o.editText.slice(0,-1)}));return}
if(k==='π'){st.update(o=>o.editText.length>=200?o:({...o,editText:o.editText+'pi'}));return}
if(k==='✓'){
const eq=s.editText.trim();if(!eq)return;
if(eq.indexOf('=')<0&&eq.indexOf('>')<0&&eq.indexOf('<')<0){
kl=true;
dialog.alert({title:'!',message:'需含=或</>符'}).then(()=>{kl=false});
return
}
const evv=be(eq);
if(!evv){
kl=true;
dialog.alert({title:'!',message:'解析失败'}).then(()=>{kl=false});
return
}
if(evv.mode==='trivial'){
kl=true;
dialog.alert({title:'!',message:evv.msg}).then(()=>{kl=false});
return
}
const ne=s.eqs.slice();
ne[s.editIndex]=eq;
if(s.reEdit){
st.update(o=>({...o,eqs:ne,phase:'draw',reEdit:false,editText:''}));
sj(st.get(),false);
return
}
const tot=s.multiCount;
if(s.editIndex+1>=tot){
st.update(o=>({...o,eqs:ne,phase:'draw',editText:''}));
sj(st.get(),false)
}else{
st.update(o=>({...o,eqs:ne,editIndex:o.editIndex+1,editText:'',editPage:0}))
}
return
}
const fk=['sin','cos','tan','log','ln','asin','acos','atan','exp','pow','sqrt','cbrt','abs','min','max','round','floor','ceil'];
st.update(o=>{
if(o.editText.length>=200)return o;
if(fk.indexOf(k)>=0)return{...o,editText:o.editText+k+'('};
return{...o,editText:o.editText+k}
})
}

function kbrow(ra,ri){
const bs=[];
for(let c=0;c<ra.length;c++){
const k=ra[c];
if(k===' '){bs.push(ui.text(' ',{width:KW,height:KH,lines:1}));continue}
let tn='neutral';
if(k==='✓')tn='primary';
if(k==='✗')tn='danger';
bs.push(ui.button(k,(function(kk){return function(){hk(kk)}})(k),{id:'kb_'+ri+'_'+c,width:KW,height:KH,tone:tn}))
}
return ui.row(bs,{id:'kbr_'+ri,gap:KG})
}

function mw(){
return ui.column([
trow('模式','#9cdcfe',18),
btn('1条',()=>{st.update(o=>({...o,mode:'single',multiCount:1,eqs:[],editIndex:0,editText:'',editPage:0,phase:'edit',reEdit:false}))},'mS','primary',56),
btn('2条',()=>{st.update(o=>({...o,mode:'multi',multiCount:2,eqs:[],editIndex:0,editText:'',editPage:0,phase:'edit',reEdit:false}))},'m2','neutral',56),
btn('3条',()=>{st.update(o=>({...o,mode:'multi',multiCount:3,eqs:[],editIndex:0,editText:'',editPage:0,phase:'edit',reEdit:false}))},'m3','neutral',56)
],{padding:16,gap:12})
}

function ew(s){
const pg=KP[s.editPage];const ch=[];
ch.push(trow((s.reEdit?'改':'输')+'方程'+(s.editIndex+1)+'/'+s.multiCount,'#9cdcfe',16));
ch.push(ui.text(s.editText||'(空)',{size:18,lineHeight:24,lines:2,color:'#ffd75e'}));
ch.push(ui.spacer(2));
for(let r=0;r<pg.length;r++)ch.push(kbrow(pg[r],r));
return ui.column(ch,{padding:8,gap:4})
}

function dw(s){
const GW=280,GH=280;
const v=mv(s,GW,GH);
const ek=ck(s);
if(!job||job.key!==ek){
if(!pend){
pend=true;
setTimeout(()=>{
pend=false;
if(!job||job.key!==ck(st.get()))sj(st.get(),true)
},0)
}
}
const aj=(job&&job.key===ek)?job:null;
const nd=[];
nd.push(ui.stack([],{width:GW,height:GH,background:'#000000',radius:4}));
const ay=v.toPy(0),ax=v.toPx(0);
if(ay>=0&&ay<=GH)nd.push(ui.stack([],{x:0,y:ay-1,width:GW,height:2,background:'#39ff39'}));
if(ax>=0&&ax<=GW)nd.push(ui.stack([],{x:ax-1,y:0,width:2,height:GH,background:'#39ff39'}));

const sX=ns(v.visX);
const stX=Math.ceil((s.cx-v.visX)/sX)*sX;
for(let vv=stX;vv<=s.cx+v.visX+1e-9;vv+=sX){
const px=Math.round(v.toPx(vv));
if(px<0||px>GW)continue;
if(ay>=0&&ay<=GH)nd.push(ui.stack([],{x:px-1,y:ay-5,width:2,height:10,background:'#39ff39'}));
if(aj&&aj.allDone&&Math.abs(vv)>1e-9&&ay>=0&&ay+25<=GH){
nd.push(ui.text(fm(vv),{x:Math.max(0,Math.min(GW-40,px-20)),y:ay+7,width:40,height:18,size:16,lineHeight:16,lines:1,color:'#89ff89',align:'center'}))
}
}
const sY=ns(v.visY);
const stY=Math.ceil((s.cy-v.visY)/sY)*sY;
for(let vv=stY;vv<=s.cy+v.visY+1e-9;vv+=sY){
const py=Math.round(v.toPy(vv));
if(py<0||py>GH)continue;
if(ax>=0&&ax<=GW)nd.push(ui.stack([],{x:ax-5,y:py-1,width:10,height:2,background:'#39ff39'}));
if(aj&&aj.allDone&&Math.abs(vv)>1e-9&&ax-48>=0){
nd.push(ui.text(fm(vv),{x:ax-48,y:py-9,width:40,height:18,size:16,lineHeight:16,lines:1,color:'#89ff89',align:'right'}))
}
}
if(ay>=0&&ay<=GH&&ax>=0&&ax<=GW)nd.push(ui.text('O',{x:ax+4,y:ay+4,width:18,height:18,size:16,lineHeight:16,lines:1,color:'#ffffff'}));

if(aj){
for(let i=0;i<aj.items.length;i++){
const it=aj.items[i];
if(it.kind!=='ineq'||!it.fillRects)continue;
const fc=IF[i%3];
for(let k=0;k<it.fillRects.length;k++){
const r=it.fillRects[k];
nd.push(ui.stack([],{x:r.x,y:r.y,width:10,height:10,background:fc,radius:0}))
}
}
for(let i=0;i<aj.items.length;i++){
const it=aj.items[i];
if(it.kind==='trivial')continue;
const cap=aj.allDone?800:150;
dp(it.pts,aj.view,nd,it.curve.color,5,cap);
if(it.dPt)dp(it.dPt,aj.view,nd,DC,4,cap);
if(it.iPt)dp(it.iPt,aj.view,nd,IC,4,cap)
}
if(aj.allDone){
for(let i=0;i<aj.extremaResults.length;i++){
const r=aj.extremaResults[i];
for(let k=0;k<r.pts.length;k++){
const p=r.pts[k];
const px=aj.view.toPx(p.x),py=aj.view.toPy(p.y);
nd.push(ui.text(p.kind==='max'?'▲':'▼',{
x:Math.max(0,Math.min(GW-14,Math.round(px)-7)),
y:Math.max(0,Math.min(GH-14,Math.round(py)+(p.kind==='max'?-16:2))),
width:14,height:14,size:12,lineHeight:12,lines:1,
color:p.kind==='max'?'#ff8800':'#00c8ff',align:'center'
}))
}
}
for(let i=0;i<aj.axisResults.length;i++){
const pts=aj.axisResults[i].pts;
for(let k=0;k<pts.length;k++){
const px=aj.view.toPx(pts[k].x),py=aj.view.toPy(pts[k].y);
nd.push(ui.stack([],{
x:Math.max(0,Math.min(GW-6,Math.round(px)-3)),
y:Math.max(0,Math.min(GH-6,Math.round(py)-3)),
width:6,height:6,background:'#ff88ff',radius:3
}))
}
}
for(let i=0;i<aj.pairResults.length;i++){
const pts=aj.pairResults[i].pts;
for(let k=0;k<pts.length;k++){
const px=aj.view.toPx(pts[k].x),py=aj.view.toPy(pts[k].y);
nd.push(ui.stack([],{
x:Math.max(0,Math.min(GW-6,Math.round(px)-3)),
y:Math.max(0,Math.min(GH-6,Math.round(py)-3)),
width:6,height:6,background:'#ffff44',radius:3
}))
}
}
if(nd.length>1400&&s.densityLevel>0&&!adp){
adp=true;
adpTimer=setTimeout(()=>{
adpTimer=null;adp=false;
st.update(o=>({...o,densityLevel:Math.max(0,o.densityLevel-1)}))
},100)
}
}
}

const er=(aj&&aj.allDone)?aj.extremaResults:[];
const ar2=(aj&&aj.allDone)?aj.axisResults:[];
const pr=(aj&&aj.allDone)?aj.pairResults:[];
const ch=[];
if(s.eqs.length===1)ch.push(ui.text(s.eqs[0],{size:16,lineHeight:20,lines:2,color:'#ffffff'}));
else for(let i=0;i<s.eqs.length;i++)ch.push(tcol((i+1)+':'+s.eqs[i],CC[i%3],14,2));
ch.push(ui.spacer(2));
ch.push(ui.stack(nd,{width:GW,height:GH}));
ch.push(ui.spacer(2));
ch.push(row([btn('放大',()=>zm(0.7),'zI','primary'),btn('缩小',()=>zm(1.4),'zO'),btn('复位',rv,'zR')]));
ch.push(row([btn('←',()=>pn(-1,0),'pL'),btn('↑',()=>pn(0,1),'pU'),btn('↓',()=>pn(0,-1),'pD'),btn('→',()=>pn(1,0),'pR')]));
ch.push(row([
btn(s.showDeriv?'导✓':'导',()=>{st.update(o=>({...o,showDeriv:!o.showDeriv}));sj(st.get(),false)},'bD',s.showDeriv?'primary':'neutral'),
btn(s.showInteg?'积✓':'积',()=>{st.update(o=>({...o,showInteg:!o.showInteg}));sj(st.get(),false)},'bI',s.showInteg?'primary':'neutral'),
btn('密'+['低','中','高'][s.densityLevel],()=>{st.update(o=>({...o,densityLevel:(o.densityLevel+1)%3}));sj(st.get(),false)},'bN')
]));
ch.push(row([
btn('自动',aa,'bA'),
btn('改范围',async()=>{
const cur=st.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;
const eq=Math.max(rx.value,ry.value);
st.update(o=>({...o,xRange:eq,yRange:eq,initX:eq,initY:eq,cx:0,cy:0}));
sj(st.get(),false)
},'bR')
]));
for(let i=0;i<s.eqs.length;i++){
ch.push(btn('改'+(i+1),(function(idx){
return function(){
st.update(o=>({...o,phase:'edit',editIndex:idx,editText:o.eqs[idx],editPage:0,reEdit:true}))
}
})(i),'bE'+i,'primary'))
}
ch.push(ui.spacer(2));
if(er.length>0){
ch.push(trow('极值','#ff8800',16));
for(let i=0;i<er.length;i++){
const r=er[i];const ps2=[];
for(let k=0;k<r.pts.length;k++){
const p=r.pts[k];
ps2.push((p.kind==='max'?'▲':'▼')+'('+fm(p.x)+','+fm(p.y)+')')
}
ch.push(tcol((r.idx+1)+':'+ps2.join(' '),'#ffcc66',14,3))
}
}
if(ar2.length>0){
ch.push(trow('轴交','#ff88ff',16));
for(let i=0;i<ar2.length;i++){
const r=ar2[i];const ps2=[];
for(let k=0;k<r.pts.length&&k<8;k++)ps2.push('('+fm(r.pts[k].x)+','+fm(r.pts[k].y)+')');
if(r.pts.length>8)ps2.push('..');
ch.push(tcol(r.label+':'+ps2.join(' '),'#ffaaff',14,3))
}
}
if(pr.length>0){
ch.push(trow('交点','#ffff44',16));
for(let i=0;i<pr.length;i++){
const r=pr[i];const ps2=[];
for(let k=0;k<r.pts.length&&k<8;k++)ps2.push('('+fm(r.pts[k].x)+','+fm(r.pts[k].y)+')');
if(r.pts.length>8)ps2.push('..');
ch.push(tcol(r.label+':'+ps2.join(' '),'#ffff88',14,3))
}
}
return ui.column(ch,{padding:8,gap:6})
}

ui.render(()=>{
const s=st.get();
if(s.phase==='mode')return mw();
if(s.phase==='edit')return ew(s);
return dw(s)
})
}
return main();