Files
Endoleap/src/lib/canvas-core/curve-hit.ts
T

35 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 有限预算的三次曲线命中精判。先用控制点包围盒排除,再用弦线距离与平坦度误差判断;
* 不能确定时用 de Casteljau 二分。预算/数值精度不足明确保留可能命中,不能伪称已证明 miss。
* 上层须提供合法曲线/点、非负半径和有效预算,本函数没有独立参数校验或 registry 检查。
*/
import type {Point} from './types.ts';
import type {Cubic} from './geometry.ts';
/** 点到线段的最短距离:先按最大坐标量级归一化,投影限制在线段上,再恢复原尺度。
* 零长度段退化为点,归一化用于减少大坐标求差/长度平方造成的溢出。 */
const distance=(p:Point,a:Point,b:Point)=>{const magnitude=Math.max(1,Math.abs(p.x),Math.abs(p.y),Math.abs(a.x),Math.abs(a.y),Math.abs(b.x),Math.abs(b.y)),x=b.x/magnitude-a.x/magnitude,y=b.y/magnitude-a.y/magnitude,dx=p.x/magnitude-a.x/magnitude,dy=p.y/magnitude-a.y/magnitude,length=Math.hypot(x,y),ux=length?x/length:0,uy=length?y/length:0,t=Math.max(0,Math.min(length,dx*ux+dy*uy));return Math.hypot(dx-t*ux,dy-t*uy)*magnitude;};
/** 两点中点写成 a/2+b/2,减少先做 a+b 时溢出的风险。 */
const mid=(a:Point,b:Point)=>({x:a.x/2+b.x/2,y:a.y/2+b.y/2});
/** de Casteljau 在 t=0.5 分割,生成两条连接于同一个 center 的等价子曲线。 */
function split([a,b,c,d]:Cubic):[Cubic,Cubic]{const ab=mid(a,b),bc=mid(b,c),cd=mid(c,d),abc=mid(ab,bc),bcd=mid(bc,cd),center=mid(abc,bcd);return[[a,ab,abc,center],[center,bcd,cd,d]];}
/** Hull rejection + bounded de Casteljau refinement; ambiguous boundaries are explicit, never a proven miss. */
/** 返回 hit、certainty、距离估计、误差和预算状态;hit=true/possible 需调用方保守处理。
* maxSteps 限总工作,maxDepth 限细分深度,error 控制可接受平坦度;stack 用显式 DFS。
* 每轮:包围盒排除→near±flat 判定→足够平/深度已满则记 possible→否则入栈左右子曲线。
* near 是到端点弦线段距离,flat 包括两控制点到该弦的偏差及 roundoff。
* 剩余 stack 非空表示总预算耗尽,仍可能有未检查的命中区域。
* certainty 只在 hit=true 时区分 confirmed/possible;hit=false 的正常返回也固定带 possible,不是“可能没命中”的三态枚举。 */
export function hitCubic(curve:Cubic,point:Point,radius:number,options:{error:number;maxDepth:number;maxSteps:number}){
// 栈中保存尚未证明可排除的子曲线,steps 按实际取出的子曲线计数;possible 记住已停止细分的模糊区域。
const stack:{curve:Cubic;depth:number}[]=[{curve,depth:0}];let steps=0,possible=false,estimate=Infinity,error=0;
while(stack.length&&steps<options.maxSteps){const item=stack.pop()!;steps++;const xs=item.curve.map(p=>p.x),ys=item.curve.map(p=>p.y),dx=Math.max(0,Math.min(...xs)-point.x,point.x-Math.max(...xs)),dy=Math.max(0,Math.min(...ys)-point.y,point.y-Math.max(...ys));if(Math.hypot(dx,dy)>radius)continue;
// 弦距离 near 与 flat 给出保守区间:near-flat 超半径可排除,near+flat 在半径内可确认。
// 数值非有限时直接返回 possible 且 budget=true;该标志此时不只是“栈还有工作”的含义。
const[a,b,c,d]=item.curve,roundoff=Number.EPSILON*64*Math.max(1,Math.abs(point.x),Math.abs(point.y),...xs.map(Math.abs),...ys.map(Math.abs)),flat=Math.max(distance(b,a,d),distance(c,a,d))+roundoff,near=distance(point,a,d);if(!Number.isFinite(flat)||!Number.isFinite(near))return{hit:true,certainty:'possible' as const,steps,distance:Infinity,error:Infinity,budget:true};if(near-flat>radius)continue;if(near+flat<=radius)return{hit:true,certainty:'confirmed' as const,steps,distance:near,error:flat,budget:false};
// 达到平坦误差或深度阈值只保留 possible,budget 可仍为 false;继续二分时先压右再压左,按左优先 DFS。
if(flat<=options.error||item.depth>=options.maxDepth){possible=true;if(near<estimate){estimate=near;error=flat;}continue;}const[left,right]=split(item.curve);stack.push({curve:right,depth:item.depth+1},{curve:left,depth:item.depth+1});
}
// estimate/error 来自已记住的最小 near 模糊区,不是所有曲线的精确最近距离;全被排除时 estimate 仍是 Infinity。
return{hit:possible||stack.length>0,certainty:'possible' as const,steps,distance:estimate,error,budget:stack.length>0};
}